From 1b8973296817045be6e525630834136e6eec1507 Mon Sep 17 00:00:00 2001 From: Giovanni Date: Tue, 27 Oct 2026 04:34:21 -0700 Subject: [PATCH] Record Folderwise provenance in project creation commits Use the approved welcome message with the installed framework version and GitHub link. Cover Git and archive installations while preserving creator attribution. Agent: codex --- README.md | 2 +- _tools/new_project.py | 35 +++++++++++++++++- _tools/selftest.py | 83 ++++++++++++++++++++++++++++++++++++++++++- docs/guide.md | 19 ++++++++++ 4 files changed, 136 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9a168d1..0c46e07 100755 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ git config core.hooksPath _tools/framework-hooks uv run _tools/new_project.py ``` -Choose a topic, describe its purpose, and set its sensitivity. Folderwise creates its Git repository, starter files, and an agent setup prompt. Give that prompt to Codex or Claude Code to shape the project, or edit the files yourself. +Choose a topic, describe its purpose, and set its sensitivity. Folderwise creates its Git repository, starter files, and an agent setup prompt. The first commit welcomes you and records the Folderwise version and GitHub link. Give that prompt to Codex or Claude Code to shape the project, or edit the files yourself. | | 🌱 Clone (recommended) | 📦 Release archive | diff --git a/_tools/new_project.py b/_tools/new_project.py index 30d48a1..eefd901 100755 --- a/_tools/new_project.py +++ b/_tools/new_project.py @@ -33,6 +33,39 @@ TEMPLATE = BASE / "_template" NAME_RE = re.compile(r"^[a-z0-9][a-z0-9-]*$") SKIP = {"_doc-template.md"} # copied verbatim, no placeholders to fill +VERSION_RE = r"[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?" + + +def framework_version() -> str: + """Identify this installation without borrowing a containing repository's version.""" + if (BASE / ".git").exists(): + result = subprocess.run( + ["git", "describe", "--tags", "--match", "v[0-9]*", "--long", "--always", "--dirty"], + cwd=BASE, capture_output=True, text=True, + ) + description = result.stdout.strip() + if result.returncode == 0 and description: + release = re.fullmatch(rf"v({VERSION_RE})-0-g[0-9a-f]+", description) + return release[1] if release else f"development ({description})" + else: + try: + info = json.loads((BASE / "RELEASE-INFO.json").read_text()) + version = info.get("version") if isinstance(info, dict) else None + if isinstance(version, str) and re.fullmatch(VERSION_RE, version): + return version + except (OSError, ValueError): + pass + return "unknown (version metadata unavailable)" + + +def creation_message() -> str: + return ( + f"Create knowledge project with Folderwise {framework_version()}\n\n" + "This repository was created with Folderwise, a Markdown-and-Git\n" + "framework for knowledge that humans read and AI agents help maintain.\n\n" + "https://github.com/TheLinuxGuy/folderwise\n\n" + "Make yourself at home. Your knowledge grows from here." + ) def ask(prompt: str, default: str | None = None) -> str: @@ -147,7 +180,7 @@ def main() -> int: dl.install_hook(dest, TOOLS) if not args.no_commit: run(dest, "git", "add", "-A") - run(dest, "git", "commit", "-q", "-m", "Scaffold project from template", + run(dest, "git", "commit", "-q", "-m", creation_message(), "-m", f"Agent: {args.by.split('/')[0]}" + (f"\nModel: {args.by.split('/')[1]}" if "/" in args.by else "")) print(f"created {dest}") prompt = setup_prompt(name, description) diff --git a/_tools/selftest.py b/_tools/selftest.py index 2efe3c8..c3de1bb 100755 --- a/_tools/selftest.py +++ b/_tools/selftest.py @@ -146,6 +146,87 @@ def s_create(): check("fresh project lints clean", rc == 0, out) +def s_create_version(): + """Inspect real first commits from clone, worktree and archive installations.""" + sandbox = CTX['root'] / 'creation-version' + source = sandbox / 'framework' + for name in ('_tools', '_template'): + shutil.copytree(CTX['root'] / name, source / name, ignore=shutil.ignore_patterns('__pycache__')) + + def g(*args, cwd=source): + rc, out = sh(['git', *args], cwd=cwd) + if rc: + raise RuntimeError(out) + return out.strip() + + def create(name, expected, *, base=source, extra=()): + rc, out = sh(['uv', 'run', '--quiet', str(base / '_tools/new_project.py'), + '--name', name, '--description', 'Fictional provenance test', '--no-prompt', *extra], cwd=base) + if '--no-commit' in extra: + log_rc, _ = sh(['git', 'rev-parse', '--verify', 'HEAD'], cwd=base / name) + check('--no-commit still creates an uncommitted project', rc == 0 and log_rc != 0, out) + return + message = g('log', '-1', '--format=%B', cwd=base / name) if rc == 0 else '' + attribution = 'Agent: codex\nModel: selftest' if '--by' in extra else 'Agent: human' + wanted = ( + f'Create knowledge project with Folderwise {expected}\n\n' + 'This repository was created with Folderwise, a Markdown-and-Git\n' + 'framework for knowledge that humans read and AI agents help maintain.\n\n' + 'https://github.com/TheLinuxGuy/folderwise\n\n' + 'Make yourself at home. Your knowledge grows from here.\n\n' + attribution + ) + check(f'creation message: {name}', rc == 0 and message == wanted, out + message) + check(f'creation preserves Git author: {name}', + rc == 0 and g('log', '-1', '--format=%an <%ae>', cwd=base / name) == 'selftest ', message) + + unknown = 'unknown (version metadata unavailable)' + create('no-metadata', unknown) + info = source / 'RELEASE-INFO.json' + for name, content, expected in ( + ('archive', json.dumps({'version': '1.2.3'}), '1.2.3'), + ('archive-prerelease', json.dumps({'version': '1.2.3-rc.1'}), '1.2.3-rc.1'), + ('bad-json', '{', unknown), ('wrong-shape', '[]', unknown), ('missing-version', '{}', unknown), + ('wrong-type', '{"version": 123}', unknown), + ('bad-version', json.dumps({'version': '1.2.3\nInjected'}), unknown), + ): + info.write_text(content) + create(name, expected) + # An enclosing repository must never supply an archive's version. + g('init', '-q', '-b', 'main', cwd=sandbox) + (sandbox / 'sentinel').write_text('parent\n') + g('add', 'sentinel', cwd=sandbox) + g('commit', '-q', '-m', 'Fictional parent', cwd=sandbox) + g('tag', 'v9.9.9', cwd=sandbox) + info.unlink() + create('inside-parent', unknown) + + g('init', '-q', '-b', 'main') + create('unborn', unknown) + (source / '.gitignore').write_text('__pycache__/\n') + g('add', '.gitignore', '_tools', '_template') + g('commit', '-q', '-m', 'Fictional framework') + describe = g('describe', '--tags', '--long', '--always', '--dirty') + create('untagged', f'development ({describe})') + g('tag', 'v1.2.3') + info.write_text('{"version": "9.9.9"}') + create('release', '1.2.3', extra=('--by', 'codex/selftest')) + tracked = source / '_tools/new_project.py' + tracked.write_text(tracked.read_text() + '\n# Fictional local change\n') + describe = g('describe', '--tags', '--long', '--always', '--dirty') + create('dirty', f'development ({describe})') + g('add', '_tools/new_project.py') + g('commit', '-q', '-m', 'Fictional development') + describe = g('describe', '--tags', '--long', '--always', '--dirty') + create('after-release', f'development ({describe})') + g('tag', 'v1.3.0-rc.1') + create('tagged-prerelease', '1.3.0-rc.1') + create('skip-commit', '', extra=('--no-commit',)) + worktree = sandbox / 'worktree' + g('worktree', 'add', '--detach', str(worktree), 'HEAD') + create('worktree-release', '1.3.0-rc.1', base=worktree) + shutil.rmtree(sandbox) + + def s_new_doc_and_index(): rc, out = tool("docs_new.py", "docs/hosts/nas.md", "--type", "inventory", "--title", "NAS inventory", "--about", "Disks and shares", "--by", "codex/selftest") @@ -3223,7 +3304,7 @@ def create(name,*extra): SECTIONS = [ - ("create", s_create), ("new_doc", s_new_doc_and_index), ("stamp", s_stamp_and_hook), ("hook", s_hook_fail_closed), + ("create", s_create), ("new_doc", s_new_doc_and_index), ("create_version", s_create_version), ("stamp", s_stamp_and_hook), ("hook", s_hook_fail_closed), ("lint", s_lint_rules), ("timezone", s_timezone), ("links", s_links), ("staged_source", s_staged_source), ("markdown_fences", s_markdown_fences), ("toc", s_toc), ("mv", s_mv), ("tables", s_tables), ("stale", s_stale_index_recent), ("sync", s_sync), ("ibridge_tasks", s_ibridge_tasks), ("ibridge_rich_adapter", s_ibridge_rich_adapter), ("ibridge", s_ibridge), ("ibridge_rename", s_ibridge_rename), ("ibridge_isolation", s_ibridge_isolation), ("ibridge_notes", s_ibridge_notes), ("ibridge_transient", s_ibridge_transient), ("ibridge_status", s_ibridge_status), ("ibridge_setup", s_ibridge_setup), ("doctor", s_doctor), ("layout", s_layout), ("cli_errors", s_cli_errors), ("failure_paths", s_failure_paths), ("uv_cache", s_uv_cache), diff --git a/docs/guide.md b/docs/guide.md index dc77352..6fb3b91 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -27,6 +27,25 @@ Makes the folder, its own git repo (branch `main`), shared agent rules, the pre- cd ~/folderwise && uv run _tools/new_project.py --name homelab-inventory --description "Inventory of home network hardware and software to allow for homelab v3 planning" --sensitivity private --by human ``` +The first commit records how the repository began: + +```text +Create knowledge project with Folderwise {version} + +This repository was created with Folderwise, a Markdown-and-Git +framework for knowledge that humans read and AI agents help maintain. + +https://github.com/TheLinuxGuy/folderwise + +Make yourself at home. Your knowledge grows from here. + +Agent: human +``` + +`{version}` comes from this installation: a clean checkout at a release tag records that version (including prereleases); other Git checkouts record `development (...)` with the Git description, including the commit and a `dirty` suffix for tracked local edits. A checkout without release tags records its commit hash. Release archives read the version from `RELEASE-INFO.json`. Missing or invalid metadata records `unknown (version metadata unavailable)` without blocking creation. An enclosing repository never supplies the version. Git metadata takes precedence over any leftover archive metadata. + +The Git author remains your configured identity. `--by /` retains the `Agent:` and `Model:` trailers. `--no-commit` creates the scaffold without an initial commit. Later framework updates do not rewrite this creation record. + Prefer to be asked the questions instead: ```bash