mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-06-10 10:13:49 +08:00
Compare commits
3 Commits
pr-2011-ga
...
pr-2006-sc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
958532920f | ||
|
|
f4ff831890 | ||
|
|
b217bbc3fa |
@@ -87,13 +87,10 @@ As of 2026-05-19:
|
||||
(`owner-approval-packet-2026-05-19.md`), preview-pack smoke digest
|
||||
`790430aef4a8`, local 2550-test suite, PR #2001 merge and GitHub Actions run
|
||||
`26102500291` success, PR #2002's owner-approval dashboard gate refresh and
|
||||
GitHub Actions run `26103853507`, PR #2004's Linear readiness evidence sync
|
||||
and GitHub Actions run `26105012698`, plus PR #2005's post-PR #2004
|
||||
evidence refresh and GitHub Actions run `26106321921`. The May 19 Linear
|
||||
sync document remains the current external project status surface, and the
|
||||
supply-chain gate now also records the `@types/node@25.7.0` pin and
|
||||
`brace-expansion` lock refresh needed for current npm audit/signature
|
||||
verification.
|
||||
GitHub Actions run `26103853507`, plus PR #2004's Linear readiness evidence
|
||||
sync and GitHub Actions run `26105012698`. The May 19 Linear sync document
|
||||
remains the current external project status surface, and the May 18 evidence
|
||||
remains the detailed supply-chain and publication-path snapshot.
|
||||
- `docs/releases/2.0.0-rc.1/operator-readiness-dashboard-2026-05-19.md`
|
||||
regenerates the ITO-44 prompt-to-artifact dashboard from live platform audit
|
||||
evidence: PR queue, issue queue, discussion queue, local worktree gate,
|
||||
@@ -121,7 +118,7 @@ As of 2026-05-19:
|
||||
finding evidence paths, ECC-Tools #78 harness policy-route linking, PR #1947
|
||||
supply-chain protection, and May 16 release-evidence
|
||||
refresh.
|
||||
- `npm run harness:audit -- --format json` reports 80/80 on current `main`.
|
||||
- `npm run harness:audit -- --format json` reports 70/70 on current `main`.
|
||||
- `npm run observability:ready` reports 21/21 readiness on current `main`,
|
||||
including the GitHub/Linear/handoff/roadmap progress-sync contract.
|
||||
- GitHub CI run `26017368895` completed successfully for
|
||||
|
||||
@@ -8,9 +8,9 @@ social announcement.
|
||||
|
||||
| Field | Evidence |
|
||||
| --- | --- |
|
||||
| Upstream main | `d6022d6b8dc5ef1393cf18ae40ee58f646f3754e` |
|
||||
| Upstream main | `ac7434ea8f39166b11e9d06ce64b38c4fb8d9202` |
|
||||
| Git remote | `https://github.com/affaan-m/ECC.git` |
|
||||
| Evidence scope | Current `main` after PR #1990 harness-audit GitHub integration scoring, PR #1991 canonical ECC identity gate, PR #1992 release video-suite gate, PR #1993 growth outreach pack, PR #1994 May 19 publication evidence refresh, PR #1995 operator dashboard refresh, PR #1996 primary render self-eval gate, PR #1997 publish-candidate gate, PR #1998 visual QA gate, PR #1999 video dashboard evidence refresh, PR #2000 suite-count evidence refresh, PR #2001 owner approval packet addition, PR #2002 owner approval dashboard gate refresh, PR #2004 Linear readiness evidence sync, and PR #2005 post-PR #2004 evidence refresh |
|
||||
| Evidence scope | Current `main` after PR #1990 harness-audit GitHub integration scoring, PR #1991 canonical ECC identity gate, PR #1992 release video-suite gate, PR #1993 growth outreach pack, PR #1994 May 19 publication evidence refresh, PR #1995 operator dashboard refresh, PR #1996 primary render self-eval gate, PR #1997 publish-candidate gate, PR #1998 visual QA gate, PR #1999 video dashboard evidence refresh, PR #2000 suite-count evidence refresh, PR #2001 owner approval packet addition, PR #2002 owner approval dashboard gate refresh, and PR #2004 Linear readiness evidence sync |
|
||||
| Local status caveat | `git status --short --branch` was clean after pulling `origin/main`; generated evidence files are committed after the source snapshot they describe |
|
||||
|
||||
The release operator must repeat all publish-facing checks from the exact final
|
||||
@@ -52,7 +52,6 @@ Tracked repositories in the platform audit were:
|
||||
| PR #2001 | Merged the final human decision sheet for release, package, plugin, video, billing, social, and outbound approvals; GitHub Actions run `26102500291` completed successfully |
|
||||
| PR #2002 | Merged the owner-approval dashboard refresh so the operator dashboard fails closed when the final decision sheet is missing or incomplete; CI passed before merge |
|
||||
| PR #2004 | Merged the May 19 Linear readiness evidence sync after PR #2002, including roadmap, dashboard, preview-pack manifest, publication evidence, operator dashboard generator, and release-surface test updates |
|
||||
| PR #2005 | Merged the post-PR #2004 evidence refresh, keeping the May 19 readiness ledger, dashboard, roadmap, and release-surface references current on `main` |
|
||||
|
||||
## Release And Growth Evidence
|
||||
|
||||
@@ -60,8 +59,7 @@ Tracked repositories in the platform audit were:
|
||||
| --- | --- | --- |
|
||||
| Release-surface tests | `node tests/docs/ecc2-release-surface.test.js` | 27 passed, 0 failed |
|
||||
| Preview-pack smoke | `npm run preview-pack:smoke -- --format json` | Ready true; digest `790430aef4a8`; 31 required artifacts; 5 passed, 0 failed |
|
||||
| Operator dashboard | `npm run operator:dashboard -- --write docs/releases/2.0.0-rc.1/operator-readiness-dashboard-2026-05-19.md` | Regenerated from the May 19 `main` baseline with platform audit ready true, 0 tracked PRs, 0 tracked issues, 0 discussion gaps, `$1,728/mo` current MRR, `$10,000/mo` target MRR, the release video suite marked current, and top actions for plugin publication, notifications, outbound approval, AgentShield, and ECC Tools billing |
|
||||
| Supply-chain verification | `npm audit --audit-level=moderate`; `npm audit signatures`; `yarn install --immutable --mode=skip-build` | Current supply-chain refresh found 0 npm vulnerabilities, verified 254 registry signatures and 30 attestations, and accepted the Yarn lock after pinning `@types/node@25.7.0` plus refreshing `brace-expansion` to `5.0.6` / `1.1.14` |
|
||||
| Operator dashboard | `npm run operator:dashboard -- --write docs/releases/2.0.0-rc.1/operator-readiness-dashboard-2026-05-19.md` | Regenerated from `ac7434ea8f39166b11e9d06ce64b38c4fb8d9202` with platform audit ready true, 0 tracked PRs, 0 tracked issues, 0 discussion gaps, `$1,728/mo` current MRR, `$10,000/mo` target MRR, the release video suite marked current, and top actions for plugin publication, notifications, outbound approval, AgentShield, and ECC Tools billing |
|
||||
| Release video suite | `npm run release:video-suite -- --format json --summary` with `ECC_VIDEO_SOURCE_ROOT` and `ECC_VIDEO_RELEASE_SUITE_ROOT` | Ready true; 15/15 source assets present; 13/13 render, timeline, caption, EDL, and segment artifacts present; 12/12 publish-candidate outputs present with zero detected black-frame segments; primary rough render self-eval passed at 144.759 seconds, 1920x1080, 1 audio stream, and 106.78 MB |
|
||||
| Full local suite | `node tests/run-all.js` | 2550 passed, 0 failed |
|
||||
| PR #1998 CI | GitHub Actions run `26099020341` | Completed successfully for `d500de1e9f11c0446b6a1349bd98b522d31f9125`; all reported checks passed, including lint, validation, security scan, coverage, GitGuardian, CodeRabbit, Cubic, and the macOS/Ubuntu/Windows test matrix |
|
||||
@@ -69,7 +67,6 @@ Tracked repositories in the platform audit were:
|
||||
| PR #2001 CI | GitHub Actions run `26102500291` | Completed successfully for `8148340ad14eb32c971346f0cb4cb9431ec0f5de`; required checks passed before merge |
|
||||
| PR #2002 CI | GitHub Actions run `26103853507` | Completed successfully before merge; required checks passed, Cubic remained non-blocking, and PR #2002 merged into `main` as `c7d662c3c68719e5ef0b5305ca3f6782b3214224` |
|
||||
| PR #2004 CI | GitHub Actions run `26105012698` | Completed successfully after rerunning the single failed Windows Node 18 yarn job; required checks passed, Cubic remained non-blocking, and PR #2004 merged into `main` as `ac7434ea8f39166b11e9d06ce64b38c4fb8d9202` |
|
||||
| PR #2005 CI | GitHub Actions run `26106321921` | Completed successfully with 37 completed jobs, 0 failed jobs, and PR #2005 merged into `main` as `d6022d6b8dc5ef1393cf18ae40ee58f646f3754e` |
|
||||
| Linear sync | Linear document `ecc-may-19-post-pr-2002-sync-64cef8f668e0` plus project comment `a6411e3a-8c8e-4a58-adba-687e77d4c543` | Project and issue lanes now record PR #2002 evidence, discussion #2003 routing, owner-approval dashboard gate, and In Progress status for ITO-47, ITO-48, ITO-49, ITO-51, ITO-54, and ITO-56 |
|
||||
| Public-path sanitization | `node scripts/ci/validate-no-personal-paths.js` through local suite and CI | Passed |
|
||||
| Markdown and whitespace | `markdownlint` focused release docs plus `git diff --check` before PR #1999 | Passed |
|
||||
@@ -111,7 +108,7 @@ Tracked repositories in the platform audit were:
|
||||
The tracked public PR queue, issue queue, discussion queue, canonical ECC
|
||||
identity, release video suite, preview pack, and growth outreach packet are
|
||||
current on May 19, 2026 for `main` through
|
||||
`d6022d6b8dc5ef1393cf18ae40ee58f646f3754e`. The remaining video work is
|
||||
`ac7434ea8f39166b11e9d06ce64b38c4fb8d9202`. The remaining video work is
|
||||
owner approval, upload, and public URL attachment, not render or QA production.
|
||||
|
||||
This improves publication readiness but does not replace the approval-gated
|
||||
|
||||
@@ -101,22 +101,22 @@ Record the exact commit SHA and command output before any publication action:
|
||||
|
||||
| Evidence | Command | Required result | Recorded output |
|
||||
| --- | --- | --- | --- |
|
||||
| Clean release branch | `git status --short --branch` | On intended release commit; no unrelated files | Post-PR #2005 baseline `d6022d6b8dc5ef1393cf18ae40ee58f646f3754e`: `## main...origin/main`; repeat from the exact final publication commit before release |
|
||||
| Clean release branch | `git status --short --branch` | On intended release commit; no unrelated files | `3304848b`: `## main...origin/main`; repeat from the exact final publication commit before release |
|
||||
| Preview-pack smoke | `npm run preview-pack:smoke` | Preview pack artifacts, Hermes boundary, final verification command list, and publication blockers pass | `publication-evidence-2026-05-19.md`: ready yes, digest `790430aef4a8`, 31 artifacts, 5 passed, 0 failed; repeat in the final strict clean-checkout release pass |
|
||||
| Harness audit | `npm run harness:audit -- --format json` | 80/80 passing | Current release gate: 80/80 across 8 applicable categories, 0 top actions |
|
||||
| Adapter scorecard | `npm run harness:adapters -- --check` | PASS | Current release gate: PASS, 11 adapters |
|
||||
| Observability readiness | `npm run observability:ready` | 21/21 passing | Current release gate: 21/21, ready true |
|
||||
| Release safety gate | `npm run observability:ready -- --format json` | Release Safety category passing with publication readiness, supply-chain, workflow security, package surface, and release-surface evidence | Current release gate keeps Release Safety passing at 3/3; repeat the JSON gate from the exact final release commit |
|
||||
| Supply-chain verification | `npm audit --audit-level=moderate`; `npm audit signatures`; `yarn install --immutable --mode=skip-build`; `cd ecc2 && cargo audit -q`; Dependabot alerts; GitGuardian Security Checks | 0 vulnerabilities/alerts, registry signatures verified, package-manager locks accepted, GitGuardian clean | Current supply-chain branch: `npm audit` found 0 vulnerabilities; `npm audit signatures` verified 254 registry signatures and 30 attestations; Yarn immutable install accepted the lock after pinning `@types/node@25.7.0` and moving `brace-expansion` to `5.0.6` / `1.1.14`; PR #2005 CI `26106321921` completed 37/37 jobs with 0 failures |
|
||||
| Root suite | `node tests/run-all.js` | 0 failures | PR #2005 CI `26106321921` completed successfully with 37/37 jobs and 0 failures; current branch reruns focused release/package/docs gates before merge |
|
||||
| Markdown lint | `npx markdownlint-cli '**/*.md' --ignore node_modules` | 0 failures | Current release gate: focused lint passed for `publication-readiness.md`, `publication-evidence-2026-05-19.md`, and `docs/ECC-2.0-GA-ROADMAP.md` |
|
||||
| Package surface | `node tests/scripts/npm-publish-surface.test.js` | 0 failures; no Python bytecode in npm tarball | Current release gate: 2/2 passed |
|
||||
| Release surface | `node tests/docs/ecc2-release-surface.test.js` | 0 failures | Current release gate: 27/27 passed after refreshing the discussion-count assertion to the post-PR #2005 baseline |
|
||||
| Harness audit | `npm run harness:audit -- --format json` | 70/70 passing | `99e01ded`: 70/70, 0 top actions |
|
||||
| Adapter scorecard | `npm run harness:adapters -- --check` | PASS | `99e01ded`: PASS, 11 adapters |
|
||||
| Observability readiness | `npm run observability:ready` | 21/21 passing | `publication-evidence-2026-05-18.md`: 21/21, ready yes |
|
||||
| Release safety gate | `npm run observability:ready -- --format json` | Release Safety category passing with publication readiness, supply-chain, workflow security, package surface, and release-surface evidence | May 18 evidence keeps release safety passing; repeat the JSON gate from the exact final release commit |
|
||||
| Supply-chain verification | `npm audit --json`; `npm audit signatures`; `cd ecc2 && cargo audit -q`; Dependabot alerts; GitGuardian Security Checks | 0 vulnerabilities/alerts, registry signatures verified, GitGuardian clean | `publication-evidence-2026-05-19.md` plus CI `26093792219`: GitGuardian and security scan passed; prior May 18 npm registry signatures and IOC scans remain the latest detailed supply-chain evidence |
|
||||
| Root suite | `node tests/run-all.js` | 0 failures | Current dashboard branch: local `node tests/run-all.js` passed 2550/2550; PR #2001 CI `26102500291` passed the previous full OS/runtime/package-manager matrix |
|
||||
| Markdown lint | `npx markdownlint-cli '**/*.md' --ignore node_modules` | 0 failures | CI `26093792219`: markdownlint passed on the growth-pack PR; rerun after any release-copy edits |
|
||||
| Package surface | `node tests/scripts/npm-publish-surface.test.js` | 0 failures; no Python bytecode in npm tarball | `2/2` passed in May 12 evidence pass |
|
||||
| Release surface | `node tests/docs/ecc2-release-surface.test.js` | 0 failures | May 19 evidence refresh: 27/27 passed after adding the video suite, partner/sponsor/talk gates, owner approval packet, and roadmap evidence mirror |
|
||||
| Optional Rust surface | `cd ecc2 && cargo test` | 0 failures or explicit deferral | `publication-evidence-2026-05-16.md`: 462/462 passed, existing warnings only |
|
||||
| Queue baseline | `node scripts/platform-audit.js --json` across trunk, AgentShield, JARVIS, ECC Tools, and ECC website | Under 20 open PRs and under 20 open issues | Post-PR #2005 baseline: platform audit ready true, 0 open PRs, 0 open issues, 0 conflicting PRs, and 0 blocking dirty files across tracked repos |
|
||||
| Discussion baseline | `node scripts/platform-audit.js --json` and `node scripts/discussion-audit.js --json` | No unmanaged active discussion queue and no answerable Q&A missing an accepted answer | Post-PR #2005 baseline: platform audit sampled 59 trunk discussions, 0 needing maintainer touch, 0 answerable discussions missing accepted answer; `docs/architecture/discussion-response-playbook.md` records response templates and security escalation rules |
|
||||
| Queue baseline | `node scripts/platform-audit.js --json` across trunk, AgentShield, JARVIS, ECC Tools, and ECC website | Under 20 open PRs and under 20 open issues | `3304848b`: platform audit ready, 0 open PRs, 0 open issues, 0 conflicting PRs, and 0 blocking dirty files |
|
||||
| Discussion baseline | `node scripts/platform-audit.js --json` and `node scripts/discussion-audit.js --json` | No unmanaged active discussion queue and no answerable Q&A missing an accepted answer | `3304848b`: platform audit sampled 58 trunk discussions, 0 needing maintainer touch, 0 answerable discussions missing accepted answer; `docs/architecture/discussion-response-playbook.md` records response templates and security escalation rules |
|
||||
| Linear roadmap | Linear project and issue readback | Detailed roadmap exists with release, security, AgentShield, ECC Tools, legacy, and observability lanes | May 18 Linear comments include ITO-57 `3fe5b2b7-c4fe-401c-a317-b40d72119cb3` and ITO-44 `fb4a4f33-6c2d-421a-bbdb-63cfad3e3ee4`; earlier evidence records the project and 16 issue lanes |
|
||||
| Operator readiness dashboard | `npm run operator:dashboard -- --json` | Current queue state mapped to macro-goal deliverables and incomplete gaps | Post-PR #2005 baseline: May 19 dashboard is current; platform audit ready true, 0 open PRs, 0 open issues, 0 discussion gaps, 0 dirty files, release video suite current, and publication gates still approval-gated |
|
||||
| Operator readiness dashboard | `npm run operator:dashboard -- --json` | Current queue state mapped to macro-goal deliverables and incomplete gaps | `3304848b`: regenerated May 19 dashboard from current main; platform audit ready true, 0 open PRs, 0 open issues, 0 discussion gaps, 0 dirty files, release video suite current, and publication gates still approval-gated |
|
||||
| Release URL ledger | `docs/releases/2.0.0-rc.1/release-url-ledger-2026-05-19.md` plus placeholder-marker scan | Live links and approval-gated links are separated before announcement copy is posted | Ledger records public repo/docs/npm/OpenAI Codex documentation URLs and blocks GitHub release/npm/plugin/billing/social URLs until approval-gated checks pass |
|
||||
| Release name and plugin publication checklist | `docs/releases/2.0.0-rc.1/release-name-plugin-publication-checklist-2026-05-18.md` | Name/package/plugin values are frozen, final-release commands are listed, and Claude/Codex publication paths cite current official docs | Checklist keeps `ECC`, `ecc-universal`, and plugin slug `ecc` for rc.1; no npm rename, npm publish, plugin tag, official listing, billing claim, or announcement before final evidence |
|
||||
|
||||
|
||||
40
package-lock.json
generated
40
package-lock.json
generated
@@ -21,7 +21,7 @@
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@opencode-ai/plugin": "^1.0.0",
|
||||
"@types/node": "25.7.0",
|
||||
"@types/node": "^25.8.0",
|
||||
"c8": "^11.0.0",
|
||||
"eslint": "^9.39.2",
|
||||
"globals": "^17.4.0",
|
||||
@@ -398,13 +398,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "25.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.7.0.tgz",
|
||||
"integrity": "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg==",
|
||||
"version": "25.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.8.0.tgz",
|
||||
"integrity": "sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.21.0"
|
||||
"undici-types": ">=7.24.0 <7.24.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/unist": {
|
||||
@@ -497,9 +497,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.14",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
|
||||
"integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
|
||||
"version": "1.1.13",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
|
||||
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1210,9 +1210,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/glob/node_modules/brace-expansion": {
|
||||
"version": "5.0.6",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
|
||||
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
|
||||
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1680,9 +1680,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/markdownlint-cli/node_modules/brace-expansion": {
|
||||
"version": "5.0.6",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
|
||||
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
|
||||
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2666,9 +2666,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/test-exclude/node_modules/brace-expansion": {
|
||||
"version": "5.0.6",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
|
||||
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
|
||||
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2745,9 +2745,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.21.0.tgz",
|
||||
"integrity": "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ==",
|
||||
"version": "7.24.6",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
|
||||
"integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
|
||||
@@ -335,7 +335,7 @@
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@opencode-ai/plugin": "^1.0.0",
|
||||
"@types/node": "25.7.0",
|
||||
"@types/node": "^25.8.0",
|
||||
"c8": "^11.0.0",
|
||||
"eslint": "^9.39.2",
|
||||
"globals": "^17.4.0",
|
||||
|
||||
@@ -119,65 +119,6 @@ function tokenize(segment) {
|
||||
return segment.split(/\s+/).filter(Boolean);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Tokenize a short allowlisted shell command while preserving quoted
|
||||
* arguments. This is intentionally smaller than a full shell parser: the
|
||||
* caller rejects shell control characters before invoking it, so this only
|
||||
* needs to keep spaces inside quotes together for read-only git commands.
|
||||
*
|
||||
* @param {string} input
|
||||
* @returns {string[] | null}
|
||||
*/
|
||||
function tokenizeAllowlistedShellWords(input) {
|
||||
const tokens = [];
|
||||
let current = '';
|
||||
let quote = null;
|
||||
let escaped = false;
|
||||
|
||||
for (const char of String(input || '')) {
|
||||
if (escaped) {
|
||||
current += char;
|
||||
escaped = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === '\\') {
|
||||
escaped = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (quote) {
|
||||
if (char === quote) {
|
||||
quote = null;
|
||||
} else {
|
||||
current += char;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === '"' || char === "'") {
|
||||
quote = char;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (/\s/.test(char)) {
|
||||
if (current) {
|
||||
tokens.push(current);
|
||||
current = '';
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
current += char;
|
||||
}
|
||||
|
||||
if (escaped) current += '\\';
|
||||
if (quote) return null;
|
||||
if (current) tokens.push(current);
|
||||
return tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip a leading path and trailing `.exe` from a command token so
|
||||
* `/usr/bin/git`, `git.exe`, and `GIT` all normalize to `git`.
|
||||
@@ -651,16 +592,8 @@ function isReadOnlyGitIntrospection(command) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const segments = splitCommandSegments(trimmed);
|
||||
if (segments.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const tokens = tokenizeAllowlistedShellWords(trimmed);
|
||||
if (!tokens) {
|
||||
return false;
|
||||
}
|
||||
if (commandBasename(tokens[0]) !== 'git' || tokens.length < 2) {
|
||||
const tokens = trimmed.split(/\s+/);
|
||||
if (tokens[0] !== 'git' || tokens.length < 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -680,7 +613,7 @@ function isReadOnlyGitIntrospection(command) {
|
||||
}
|
||||
|
||||
if (subcommand === 'show') {
|
||||
return args.length === 1 && !args[0].startsWith('--') && /^[a-zA-Z0-9._:/ -]+$/.test(args[0]);
|
||||
return args.length === 1 && !args[0].startsWith('--') && /^[a-zA-Z0-9._:/-]+$/.test(args[0]);
|
||||
}
|
||||
|
||||
if (subcommand === 'branch') {
|
||||
|
||||
@@ -116,13 +116,7 @@ except(KeyError, TypeError, ValueError):
|
||||
# If cwd was provided in stdin, use it for project detection
|
||||
if [ -n "$STDIN_CWD" ] && [ -d "$STDIN_CWD" ]; then
|
||||
_GIT_ROOT=$(git -C "$STDIN_CWD" rev-parse --show-toplevel 2>/dev/null || true)
|
||||
if [ -n "$_GIT_ROOT" ]; then
|
||||
export CLAUDE_PROJECT_DIR="$_GIT_ROOT"
|
||||
unset CLV2_NO_PROJECT
|
||||
else
|
||||
unset CLAUDE_PROJECT_DIR
|
||||
export CLV2_NO_PROJECT=1
|
||||
fi
|
||||
export CLAUDE_PROJECT_DIR="${_GIT_ROOT:-$STDIN_CWD}"
|
||||
fi
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
|
||||
@@ -75,42 +75,16 @@ _clv2_normalize_remote_url() {
|
||||
fi
|
||||
}
|
||||
|
||||
_clv2_main_worktree_root() {
|
||||
local root="$1"
|
||||
[ -z "$root" ] && return 0
|
||||
command -v git >/dev/null 2>&1 || return 0
|
||||
|
||||
git -C "$root" worktree list --porcelain 2>/dev/null | while IFS= read -r line; do
|
||||
case "$line" in
|
||||
worktree\ *)
|
||||
printf '%s\n' "${line#worktree }"
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
_clv2_detect_project() {
|
||||
local project_root=""
|
||||
local project_name=""
|
||||
local project_id=""
|
||||
local source_hint=""
|
||||
|
||||
if [ "${CLV2_NO_PROJECT:-0}" = "1" ]; then
|
||||
_CLV2_PROJECT_ID="global"
|
||||
_CLV2_PROJECT_NAME="global"
|
||||
_CLV2_PROJECT_ROOT=""
|
||||
_CLV2_PROJECT_DIR="${_CLV2_HOMUNCULUS_DIR}"
|
||||
mkdir -p "$_CLV2_PROJECT_DIR"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# 1. Try CLAUDE_PROJECT_DIR env var
|
||||
if [ -n "$CLAUDE_PROJECT_DIR" ] && [ -d "$CLAUDE_PROJECT_DIR" ] && command -v git &>/dev/null; then
|
||||
project_root=$(git -C "$CLAUDE_PROJECT_DIR" rev-parse --show-toplevel 2>/dev/null || true)
|
||||
if [ -n "$project_root" ]; then
|
||||
source_hint="env"
|
||||
fi
|
||||
if [ -n "$CLAUDE_PROJECT_DIR" ] && [ -d "$CLAUDE_PROJECT_DIR" ]; then
|
||||
project_root="$CLAUDE_PROJECT_DIR"
|
||||
source_hint="env"
|
||||
fi
|
||||
|
||||
# 2. Try git repo root from CWD (only if git is available)
|
||||
@@ -127,7 +101,6 @@ _clv2_detect_project() {
|
||||
_CLV2_PROJECT_NAME="global"
|
||||
_CLV2_PROJECT_ROOT=""
|
||||
_CLV2_PROJECT_DIR="${_CLV2_HOMUNCULUS_DIR}"
|
||||
mkdir -p "$_CLV2_PROJECT_DIR"
|
||||
return 0
|
||||
fi
|
||||
|
||||
@@ -160,14 +133,7 @@ _clv2_detect_project() {
|
||||
normalized_remote=$(_clv2_normalize_remote_url "$remote_url")
|
||||
fi
|
||||
|
||||
local fallback_root="$project_root"
|
||||
if [ -z "$remote_url" ]; then
|
||||
local main_worktree_root
|
||||
main_worktree_root=$(_clv2_main_worktree_root "$project_root")
|
||||
[ -n "$main_worktree_root" ] && fallback_root="$main_worktree_root"
|
||||
fi
|
||||
|
||||
local hash_input="${normalized_remote:-${remote_url:-$fallback_root}}"
|
||||
local hash_input="${normalized_remote:-${remote_url:-$project_root}}"
|
||||
# Prefer Python for consistent SHA256 behavior across shells/platforms.
|
||||
# Pass the value via env var and encode as UTF-8 inside Python so the hash
|
||||
# is locale-independent (shells vary between UTF-8 / CP932 / CP1252, which
|
||||
|
||||
@@ -22,7 +22,6 @@ import os
|
||||
import subprocess
|
||||
import sys
|
||||
import re
|
||||
import shutil
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timedelta, timezone
|
||||
@@ -195,64 +194,26 @@ def _yaml_quote(value: str) -> str:
|
||||
# Project Detection (Python equivalent of detect-project.sh)
|
||||
# ─────────────────────────────────────────────
|
||||
|
||||
def _git_repo_root(cwd: Optional[str] = None) -> Optional[str]:
|
||||
args = ["git"]
|
||||
if cwd:
|
||||
args.extend(["-C", cwd])
|
||||
args.extend(["rev-parse", "--show-toplevel"])
|
||||
try:
|
||||
result = subprocess.run(args, capture_output=True, text=True, timeout=5)
|
||||
if result.returncode == 0:
|
||||
return result.stdout.strip()
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError):
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def _main_worktree_root(project_root: str) -> str:
|
||||
"""Return the main worktree root when project_root is a linked worktree."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "-C", project_root, "worktree", "list", "--porcelain"],
|
||||
capture_output=True, text=True, timeout=5
|
||||
)
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError):
|
||||
return project_root
|
||||
|
||||
if result.returncode != 0:
|
||||
return project_root
|
||||
|
||||
for line in result.stdout.splitlines():
|
||||
if line.startswith("worktree "):
|
||||
main_root = line.split(" ", 1)[1].strip()
|
||||
return main_root or project_root
|
||||
return project_root
|
||||
|
||||
|
||||
def detect_project() -> dict:
|
||||
"""Detect current project context. Returns dict with id, name, root, project_dir."""
|
||||
project_root = None
|
||||
|
||||
if os.environ.get("CLV2_NO_PROJECT") == "1":
|
||||
return {
|
||||
"id": "global",
|
||||
"name": "global",
|
||||
"root": "",
|
||||
"project_dir": HOMUNCULUS_DIR,
|
||||
"instincts_personal": GLOBAL_PERSONAL_DIR,
|
||||
"instincts_inherited": GLOBAL_INHERITED_DIR,
|
||||
"evolved_dir": GLOBAL_EVOLVED_DIR,
|
||||
"observations_file": GLOBAL_OBSERVATIONS_FILE,
|
||||
}
|
||||
|
||||
# 1. CLAUDE_PROJECT_DIR env var
|
||||
env_dir = os.environ.get("CLAUDE_PROJECT_DIR")
|
||||
if env_dir and os.path.isdir(env_dir):
|
||||
project_root = _git_repo_root(env_dir)
|
||||
project_root = env_dir
|
||||
|
||||
# 2. git repo root
|
||||
if not project_root:
|
||||
project_root = _git_repo_root()
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "--show-toplevel"],
|
||||
capture_output=True, text=True, timeout=5
|
||||
)
|
||||
if result.returncode == 0:
|
||||
project_root = result.stdout.strip()
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError):
|
||||
pass
|
||||
|
||||
# Normalize: strip trailing slashes to keep basename and hash stable
|
||||
if project_root:
|
||||
@@ -289,10 +250,9 @@ def detect_project() -> dict:
|
||||
if remote_url:
|
||||
remote_url = _strip_remote_credentials(remote_url)
|
||||
|
||||
fallback_root = _main_worktree_root(project_root) if not remote_url else project_root
|
||||
legacy_hash_source = remote_url if remote_url else project_root
|
||||
normalized_remote = _normalize_remote_url(remote_url) if remote_url else ""
|
||||
hash_source = normalized_remote if normalized_remote else (remote_url if remote_url else fallback_root)
|
||||
hash_source = normalized_remote if normalized_remote else legacy_hash_source
|
||||
project_id = _project_hash(hash_source)
|
||||
|
||||
project_dir = PROJECTS_DIR / project_id
|
||||
@@ -392,26 +352,6 @@ def load_registry() -> dict:
|
||||
return {}
|
||||
|
||||
|
||||
def _write_registry(registry: dict) -> None:
|
||||
"""Write the project registry atomically."""
|
||||
REGISTRY_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
tmp_file = REGISTRY_FILE.parent / f".{REGISTRY_FILE.name}.tmp.{os.getpid()}"
|
||||
with open(tmp_file, "w", encoding="utf-8") as f:
|
||||
json.dump(registry, f, indent=2)
|
||||
f.write("\n")
|
||||
f.flush()
|
||||
os.fsync(f.fileno())
|
||||
os.replace(tmp_file, REGISTRY_FILE)
|
||||
|
||||
|
||||
def _validate_project_id(project_id: str) -> bool:
|
||||
if not project_id or len(project_id) > 128:
|
||||
return False
|
||||
if "/" in project_id or "\\" in project_id or ".." in project_id:
|
||||
return False
|
||||
return bool(re.match(r"^[A-Za-z0-9][A-Za-z0-9._-]*$", project_id))
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# Instinct Parser
|
||||
# ─────────────────────────────────────────────
|
||||
@@ -496,96 +436,6 @@ def _load_instincts_from_dir(directory: Path, source_type: str, scope_label: str
|
||||
return instincts
|
||||
|
||||
|
||||
def _project_counts(project_id: str) -> dict:
|
||||
project_dir = PROJECTS_DIR / project_id
|
||||
personal_dir = project_dir / "instincts" / "personal"
|
||||
inherited_dir = project_dir / "instincts" / "inherited"
|
||||
observations_file = project_dir / "observations.jsonl"
|
||||
|
||||
personal_count = len(_load_instincts_from_dir(personal_dir, "personal", "project"))
|
||||
inherited_count = len(_load_instincts_from_dir(inherited_dir, "inherited", "project"))
|
||||
observations_count = 0
|
||||
if observations_file.exists():
|
||||
try:
|
||||
with open(observations_file, encoding="utf-8") as f:
|
||||
observations_count = sum(1 for _ in f)
|
||||
except OSError:
|
||||
observations_count = 0
|
||||
|
||||
return {
|
||||
"personal": personal_count,
|
||||
"inherited": inherited_count,
|
||||
"observations": observations_count,
|
||||
"total": personal_count + inherited_count + observations_count,
|
||||
}
|
||||
|
||||
|
||||
def _remove_project_storage(project_id: str) -> None:
|
||||
project_dir = PROJECTS_DIR / project_id
|
||||
if project_dir.exists():
|
||||
shutil.rmtree(project_dir)
|
||||
|
||||
|
||||
def _project_instinct_ids(project_dir: Path, source_type: str) -> set[str]:
|
||||
instinct_dir = project_dir / "instincts" / source_type
|
||||
return {
|
||||
inst.get("id")
|
||||
for inst in _load_instincts_from_dir(instinct_dir, source_type, "project")
|
||||
if inst.get("id")
|
||||
}
|
||||
|
||||
|
||||
def _merge_instinct_dir(from_dir: Path, into_dir: Path, existing_ids: set[str]) -> tuple[int, int]:
|
||||
moved = 0
|
||||
skipped = 0
|
||||
if not from_dir.exists():
|
||||
return moved, skipped
|
||||
|
||||
into_dir.mkdir(parents=True, exist_ok=True)
|
||||
for file_path in sorted(from_dir.iterdir()):
|
||||
if not file_path.is_file() or file_path.suffix.lower() not in ALLOWED_INSTINCT_EXTENSIONS:
|
||||
continue
|
||||
try:
|
||||
instincts = parse_instinct_file(file_path.read_text(encoding="utf-8"))
|
||||
except (OSError, UnicodeDecodeError):
|
||||
instincts = []
|
||||
instinct_ids = [inst.get("id") for inst in instincts if inst.get("id")]
|
||||
if any(instinct_id in existing_ids for instinct_id in instinct_ids):
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
target_path = into_dir / file_path.name
|
||||
if target_path.exists():
|
||||
target_path = into_dir / f"{file_path.stem}-{_project_hash(str(file_path))}{file_path.suffix}"
|
||||
shutil.copy2(file_path, target_path)
|
||||
existing_ids.update(instinct_ids)
|
||||
moved += 1
|
||||
|
||||
return moved, skipped
|
||||
|
||||
|
||||
def _append_observations(from_project_dir: Path, into_project_dir: Path) -> int:
|
||||
from_file = from_project_dir / "observations.jsonl"
|
||||
if not from_file.exists():
|
||||
return 0
|
||||
|
||||
into_file = into_project_dir / "observations.jsonl"
|
||||
into_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
try:
|
||||
lines = from_file.read_text(encoding="utf-8").splitlines()
|
||||
except (OSError, UnicodeDecodeError):
|
||||
return 0
|
||||
|
||||
if not lines:
|
||||
return 0
|
||||
|
||||
with open(into_file, "a", encoding="utf-8") as f:
|
||||
for line in lines:
|
||||
if line.strip():
|
||||
f.write(line.rstrip("\n") + "\n")
|
||||
return len([line for line in lines if line.strip()])
|
||||
|
||||
|
||||
def load_all_instincts(project: dict, include_global: bool = True) -> list[dict]:
|
||||
"""Load all instincts: project-scoped + global.
|
||||
|
||||
@@ -1330,14 +1180,7 @@ def _promote_auto(project: dict, force: bool, dry_run: bool) -> int:
|
||||
# ─────────────────────────────────────────────
|
||||
|
||||
def cmd_projects(args) -> int:
|
||||
"""List or maintain known projects and their instinct counts."""
|
||||
if getattr(args, "project_action", None) == "delete":
|
||||
return _cmd_projects_delete(args)
|
||||
if getattr(args, "project_action", None) == "merge":
|
||||
return _cmd_projects_merge(args)
|
||||
if getattr(args, "project_action", None) == "gc":
|
||||
return _cmd_projects_gc(args)
|
||||
|
||||
"""List all known projects and their instinct counts."""
|
||||
registry = load_registry()
|
||||
|
||||
if not registry:
|
||||
@@ -1382,143 +1225,6 @@ def cmd_projects(args) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
def _cmd_projects_delete(args) -> int:
|
||||
registry = load_registry()
|
||||
project_id = args.project_id
|
||||
|
||||
if not _validate_project_id(project_id):
|
||||
print(f"Invalid project ID: {project_id}", file=sys.stderr)
|
||||
return 1
|
||||
if project_id not in registry and not (PROJECTS_DIR / project_id).exists():
|
||||
print(f"Project '{project_id}' not found.", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
counts = _project_counts(project_id)
|
||||
print(f"Project: {project_id}")
|
||||
print(f" Instincts: {counts['personal']} personal, {counts['inherited']} inherited")
|
||||
print(f" Observations: {counts['observations']} events")
|
||||
|
||||
if args.dry_run:
|
||||
print(f"\n[DRY RUN] Would delete project '{project_id}' from registry and storage.")
|
||||
return 0
|
||||
|
||||
if not args.force:
|
||||
if counts["total"] > 0:
|
||||
print("\nWarning: this project has instincts or observations.")
|
||||
response = input(f"Delete project '{project_id}'? [y/N] ")
|
||||
if response.lower() != "y":
|
||||
print("Cancelled.")
|
||||
return 0
|
||||
|
||||
registry.pop(project_id, None)
|
||||
_write_registry(registry)
|
||||
_remove_project_storage(project_id)
|
||||
print(f"\nDeleted project '{project_id}'.")
|
||||
return 0
|
||||
|
||||
|
||||
def _cmd_projects_gc(args) -> int:
|
||||
registry = load_registry()
|
||||
candidates = [
|
||||
project_id
|
||||
for project_id in sorted(registry)
|
||||
if _validate_project_id(project_id) and _project_counts(project_id)["total"] == 0
|
||||
]
|
||||
|
||||
if not candidates:
|
||||
print("No zero-value project entries found.")
|
||||
return 0
|
||||
|
||||
print(f"Zero-value project entries: {len(candidates)}")
|
||||
for project_id in candidates:
|
||||
pinfo = registry.get(project_id, {})
|
||||
print(f" - {pinfo.get('name', project_id)} [{project_id}]")
|
||||
|
||||
if args.dry_run:
|
||||
print(f"\n[DRY RUN] Would delete {len(candidates)} project entr{'y' if len(candidates) == 1 else 'ies'}.")
|
||||
return 0
|
||||
|
||||
if not args.force:
|
||||
response = input(f"\nDelete {len(candidates)} zero-value project entr{'y' if len(candidates) == 1 else 'ies'}? [y/N] ")
|
||||
if response.lower() != "y":
|
||||
print("Cancelled.")
|
||||
return 0
|
||||
|
||||
for project_id in candidates:
|
||||
registry.pop(project_id, None)
|
||||
_remove_project_storage(project_id)
|
||||
_write_registry(registry)
|
||||
print(f"\nDeleted {len(candidates)} zero-value project entr{'y' if len(candidates) == 1 else 'ies'}.")
|
||||
return 0
|
||||
|
||||
|
||||
def _cmd_projects_merge(args) -> int:
|
||||
from_id = args.from_id
|
||||
into_id = args.into_id
|
||||
|
||||
if not _validate_project_id(from_id) or not _validate_project_id(into_id):
|
||||
print("Invalid project ID.", file=sys.stderr)
|
||||
return 1
|
||||
if from_id == into_id:
|
||||
print("Cannot merge a project into itself.", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
registry = load_registry()
|
||||
if from_id not in registry:
|
||||
print(f"Source project '{from_id}' not found.", file=sys.stderr)
|
||||
return 1
|
||||
if into_id not in registry:
|
||||
print(f"Destination project '{into_id}' not found.", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
from_counts = _project_counts(from_id)
|
||||
into_counts = _project_counts(into_id)
|
||||
print(f"Merge: {from_id} -> {into_id}")
|
||||
print(f" Source: {from_counts['personal']} personal, {from_counts['inherited']} inherited, {from_counts['observations']} observations")
|
||||
print(f" Destination before merge: {into_counts['personal']} personal, {into_counts['inherited']} inherited, {into_counts['observations']} observations")
|
||||
|
||||
if args.dry_run:
|
||||
print("\n[DRY RUN] Would merge source project into destination and remove source.")
|
||||
return 0
|
||||
|
||||
if not args.force:
|
||||
response = input(f"\nMerge '{from_id}' into '{into_id}' and remove source? [y/N] ")
|
||||
if response.lower() != "y":
|
||||
print("Cancelled.")
|
||||
return 0
|
||||
|
||||
from_project_dir = PROJECTS_DIR / from_id
|
||||
into_project_dir = PROJECTS_DIR / into_id
|
||||
into_project_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
personal_existing = _project_instinct_ids(into_project_dir, "personal")
|
||||
inherited_existing = _project_instinct_ids(into_project_dir, "inherited")
|
||||
personal_moved, personal_skipped = _merge_instinct_dir(
|
||||
from_project_dir / "instincts" / "personal",
|
||||
into_project_dir / "instincts" / "personal",
|
||||
personal_existing,
|
||||
)
|
||||
inherited_moved, inherited_skipped = _merge_instinct_dir(
|
||||
from_project_dir / "instincts" / "inherited",
|
||||
into_project_dir / "instincts" / "inherited",
|
||||
inherited_existing,
|
||||
)
|
||||
observations_moved = _append_observations(from_project_dir, into_project_dir)
|
||||
|
||||
registry.pop(from_id, None)
|
||||
destination = registry.get(into_id, {})
|
||||
destination["last_seen"] = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
||||
registry[into_id] = destination
|
||||
_write_registry(registry)
|
||||
_remove_project_storage(from_id)
|
||||
|
||||
print("\nMerged project registry entry.")
|
||||
print(f" Moved instincts: {personal_moved + inherited_moved}")
|
||||
print(f" Skipped duplicate instincts: {personal_skipped + inherited_skipped}")
|
||||
print(f" Appended observations: {observations_moved}")
|
||||
return 0
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# Generate Evolved Structures
|
||||
# ─────────────────────────────────────────────
|
||||
@@ -1780,19 +1486,6 @@ def main() -> int:
|
||||
|
||||
# Projects (new in v2.1)
|
||||
projects_parser = subparsers.add_parser('projects', help='List known projects and instinct counts')
|
||||
projects_subparsers = projects_parser.add_subparsers(dest='project_action')
|
||||
projects_delete = projects_subparsers.add_parser('delete', help='Delete a project registry entry')
|
||||
projects_delete.add_argument('project_id', help='Project ID to delete')
|
||||
projects_delete.add_argument('--dry-run', action='store_true', help='Preview without deleting')
|
||||
projects_delete.add_argument('--force', action='store_true', help='Skip confirmation')
|
||||
projects_merge = projects_subparsers.add_parser('merge', help='Merge one project registry entry into another')
|
||||
projects_merge.add_argument('from_id', help='Source project ID')
|
||||
projects_merge.add_argument('into_id', help='Destination project ID')
|
||||
projects_merge.add_argument('--dry-run', action='store_true', help='Preview without merging')
|
||||
projects_merge.add_argument('--force', action='store_true', help='Skip confirmation')
|
||||
projects_gc = projects_subparsers.add_parser('gc', help='Delete zero-value project registry entries')
|
||||
projects_gc.add_argument('--dry-run', action='store_true', help='Preview without deleting')
|
||||
projects_gc.add_argument('--force', action='store_true', help='Skip confirmation')
|
||||
|
||||
# Prune (pending instinct TTL)
|
||||
prune_parser = subparsers.add_parser('prune', help='Delete pending instincts older than TTL')
|
||||
|
||||
@@ -462,7 +462,7 @@ test('publication readiness checklist gates public release actions on evidence',
|
||||
assert.ok(source.includes('release-name-plugin-publication-checklist-2026-05-18.md'));
|
||||
assert.ok(source.includes('Release name and plugin publication checklist'));
|
||||
assert.ok(may15Evidence.includes('| Trunk discussions | GraphQL discussion count and maintainer-touch sweep | 58 total discussions;'));
|
||||
assert.ok(source.includes('platform audit sampled 59 trunk discussions'));
|
||||
assert.ok(source.includes('platform audit sampled 58 trunk discussions'));
|
||||
assert.ok(source.includes('0 needing maintainer touch'));
|
||||
assert.ok(source.includes('discussion-response-playbook.md'));
|
||||
for (const expected of [
|
||||
|
||||
@@ -181,14 +181,29 @@ test('detect-project.sh sets PROJECT_NAME and non-global PROJECT_ID for worktree
|
||||
}
|
||||
});
|
||||
|
||||
// Create a worktree-like directory with .git as a file
|
||||
const worktreeDir = path.join(testDir, 'my-worktree');
|
||||
execSync(`git worktree add "${worktreeDir}" -b feature/project-id`, {
|
||||
cwd: mainRepo,
|
||||
stdio: 'pipe'
|
||||
});
|
||||
assert.ok(
|
||||
fs.statSync(path.join(worktreeDir, '.git')).isFile(),
|
||||
'linked worktree should expose .git as a file'
|
||||
fs.mkdirSync(worktreeDir, { recursive: true });
|
||||
|
||||
// Set up the worktree directory structure in the main repo
|
||||
const worktreesDir = path.join(mainRepo, '.git', 'worktrees', 'my-worktree');
|
||||
fs.mkdirSync(worktreesDir, { recursive: true });
|
||||
|
||||
// Create the gitdir file and commondir in the worktree metadata
|
||||
const mainGitDir = path.join(mainRepo, '.git');
|
||||
fs.writeFileSync(
|
||||
path.join(worktreesDir, 'commondir'),
|
||||
'../..\n'
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(worktreesDir, 'HEAD'),
|
||||
fs.readFileSync(path.join(mainGitDir, 'HEAD'), 'utf8')
|
||||
);
|
||||
|
||||
// Write .git file in the worktree directory (this is what git worktree creates)
|
||||
fs.writeFileSync(
|
||||
path.join(worktreeDir, '.git'),
|
||||
`gitdir: ${worktreesDir}\n`
|
||||
);
|
||||
|
||||
// Source detect-project.sh from the worktree directory and capture results
|
||||
@@ -233,61 +248,6 @@ test('detect-project.sh sets PROJECT_NAME and non-global PROJECT_ID for worktree
|
||||
}
|
||||
});
|
||||
|
||||
test('detect-project.sh uses the main worktree hash when no remote exists', () => {
|
||||
const testDir = createTempDir();
|
||||
|
||||
try {
|
||||
const mainRepo = path.join(testDir, 'main-repo');
|
||||
const worktreeDir = path.join(testDir, 'feature-worktree');
|
||||
const homeDir = path.join(testDir, 'home');
|
||||
fs.mkdirSync(mainRepo, { recursive: true });
|
||||
fs.mkdirSync(homeDir, { recursive: true });
|
||||
execSync('git init', { cwd: mainRepo, stdio: 'pipe' });
|
||||
execSync('git commit --allow-empty -m "init"', {
|
||||
cwd: mainRepo,
|
||||
stdio: 'pipe',
|
||||
env: {
|
||||
...process.env,
|
||||
GIT_AUTHOR_NAME: 'Test',
|
||||
GIT_AUTHOR_EMAIL: 'test@test.com',
|
||||
GIT_COMMITTER_NAME: 'Test',
|
||||
GIT_COMMITTER_EMAIL: 'test@test.com'
|
||||
}
|
||||
});
|
||||
execSync(`git worktree add "${worktreeDir}" -b feature/no-remote`, {
|
||||
cwd: mainRepo,
|
||||
stdio: 'pipe'
|
||||
});
|
||||
|
||||
function detectId(targetDir) {
|
||||
const script = `
|
||||
export HOME="${toBashPath(homeDir)}"
|
||||
export USERPROFILE="${toBashPath(homeDir)}"
|
||||
export CLAUDE_PROJECT_DIR="${toBashPath(targetDir)}"
|
||||
source "${toBashPath(detectProjectPath)}" >/dev/null
|
||||
printf "%s" "$PROJECT_ID"
|
||||
`;
|
||||
return execFileSync('bash', ['-lc', script], {
|
||||
cwd: targetDir,
|
||||
timeout: 10000,
|
||||
env: {
|
||||
...process.env,
|
||||
HOME: toBashPath(homeDir),
|
||||
USERPROFILE: toBashPath(homeDir),
|
||||
CLAUDE_PROJECT_DIR: toBashPath(targetDir)
|
||||
}
|
||||
}).toString();
|
||||
}
|
||||
|
||||
const mainId = detectId(mainRepo);
|
||||
const worktreeId = detectId(worktreeDir);
|
||||
assert.ok(mainId && mainId !== 'global', 'main repo should get a project id');
|
||||
assert.strictEqual(worktreeId, mainId, 'linked worktree should share the main worktree project id');
|
||||
} finally {
|
||||
cleanupDir(testDir);
|
||||
}
|
||||
});
|
||||
|
||||
// ──────────────────────────────────────────────────────
|
||||
// Summary
|
||||
// ──────────────────────────────────────────────────────
|
||||
|
||||
@@ -769,8 +769,6 @@ function runTests() {
|
||||
'git diff --name-only',
|
||||
'git log --oneline --max-count=1',
|
||||
'git show HEAD:README.md',
|
||||
'git show HEAD:"docs/install guide.md"',
|
||||
'/usr/bin/git status --short',
|
||||
'git branch --show-current',
|
||||
'git rev-parse --abbrev-ref HEAD',
|
||||
];
|
||||
@@ -804,20 +802,7 @@ function runTests() {
|
||||
assert.ok(output.hookSpecificOutput.permissionDecisionReason.includes('current user request'));
|
||||
})) passed++; else failed++;
|
||||
|
||||
// --- Test 23: quoted shell separators are not read-only git bypasses
|
||||
clearState();
|
||||
if (test('does not treat quoted shell separators as read-only git introspection', () => {
|
||||
const result = runBashHook({
|
||||
tool_name: 'Bash',
|
||||
tool_input: { command: 'git show HEAD:"docs/a;b.md"' }
|
||||
});
|
||||
const output = parseOutput(result.stdout);
|
||||
assert.ok(output, 'should produce valid JSON output');
|
||||
assert.strictEqual(output.hookSpecificOutput.permissionDecision, 'deny');
|
||||
assert.ok(output.hookSpecificOutput.permissionDecisionReason.includes('current user request'));
|
||||
})) passed++; else failed++;
|
||||
|
||||
// --- Test 24: module-load pruning removes old state files only ---
|
||||
// --- Test 23: module-load pruning removes old state files only ---
|
||||
clearState();
|
||||
if (test('prunes stale state files while keeping fresh state files', () => {
|
||||
const staleFile = path.join(stateDir, 'state-stale-session.json');
|
||||
|
||||
@@ -3300,14 +3300,11 @@ async function runTests() {
|
||||
|
||||
assert.strictEqual(result.code, 0, `observe.sh should exit successfully, stderr: ${result.stderr}`);
|
||||
|
||||
const homunculusDir = path.join(homeDir, '.local', 'share', 'ecc-homunculus');
|
||||
const projectsDir = path.join(homunculusDir, 'projects');
|
||||
assert.ok(
|
||||
!fs.existsSync(projectsDir) || fs.readdirSync(projectsDir).length === 0,
|
||||
'observe.sh should not create a project-scoped directory for a non-git cwd'
|
||||
);
|
||||
const projectsDir = path.join(homeDir, '.local', 'share', 'ecc-homunculus', 'projects');
|
||||
const projectIds = fs.readdirSync(projectsDir);
|
||||
assert.strictEqual(projectIds.length, 1, 'observe.sh should create one project-scoped observation directory');
|
||||
|
||||
const observationsPath = path.join(homunculusDir, 'observations.jsonl');
|
||||
const observationsPath = path.join(projectsDir, projectIds[0], 'observations.jsonl');
|
||||
const observations = fs.readFileSync(observationsPath, 'utf8').trim().split('\n').filter(Boolean);
|
||||
assert.ok(observations.length > 0, 'observe.sh should append at least one observation');
|
||||
|
||||
|
||||
@@ -135,8 +135,8 @@ test('observe.sh resolves cwd to git root before setting CLAUDE_PROJECT_DIR', ()
|
||||
'observe.sh should resolve STDIN_CWD to git repo root'
|
||||
);
|
||||
assert.ok(
|
||||
content.includes('export CLV2_NO_PROJECT=1'),
|
||||
'observe.sh should mark non-git cwd payloads as global instead of registering raw cwd'
|
||||
content.includes('${_GIT_ROOT:-$STDIN_CWD}'),
|
||||
'observe.sh should fall back to raw cwd when git root is unavailable'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -250,7 +250,7 @@ test('observe.sh falls back to CLAUDE_HOOK_EVENT_NAME when no phase argument is
|
||||
}
|
||||
});
|
||||
|
||||
test('observe.sh records non-git cwd payloads globally without project registry side effects', () => {
|
||||
test('observe.sh keeps the raw cwd when the directory is not inside a git repo', () => {
|
||||
const testRoot = createTempDir();
|
||||
|
||||
try {
|
||||
@@ -262,17 +262,12 @@ test('observe.sh records non-git cwd payloads globally without project registry
|
||||
const result = runObserve({ homeDir, cwd: nonGitDir });
|
||||
assert.strictEqual(result.status, 0, result.stderr);
|
||||
|
||||
const homunculusDir = path.join(homeDir, '.local', 'share', 'ecc-homunculus');
|
||||
const projectsDir = path.join(homunculusDir, 'projects');
|
||||
const registryPath = path.join(homunculusDir, 'projects.json');
|
||||
const observationsPath = path.join(homunculusDir, 'observations.jsonl');
|
||||
|
||||
assert.ok(!fs.existsSync(registryPath), 'non-git cwd should not create projects.json');
|
||||
assert.ok(
|
||||
!fs.existsSync(projectsDir) || fs.readdirSync(projectsDir).length === 0,
|
||||
'non-git cwd should not create project directories'
|
||||
const { metadata } = readSingleProjectMetadata(homeDir);
|
||||
assert.strictEqual(
|
||||
normalizeComparablePath(metadata.root),
|
||||
normalizeComparablePath(nonGitDir),
|
||||
'project metadata root should stay on the non-git cwd'
|
||||
);
|
||||
assert.ok(fs.existsSync(observationsPath), 'non-git cwd should still record a global observation');
|
||||
} finally {
|
||||
cleanupDir(testRoot);
|
||||
}
|
||||
|
||||
@@ -1,260 +0,0 @@
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const { spawnSync } = require('child_process');
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
const repoRoot = path.resolve(__dirname, '..', '..');
|
||||
const cliPath = path.join(
|
||||
repoRoot,
|
||||
'skills',
|
||||
'continuous-learning-v2',
|
||||
'scripts',
|
||||
'instinct-cli.py'
|
||||
);
|
||||
|
||||
function test(name, fn) {
|
||||
try {
|
||||
fn();
|
||||
console.log(` ✓ ${name}`);
|
||||
passed += 1;
|
||||
} catch (error) {
|
||||
console.log(` ✗ ${name}`);
|
||||
console.log(` Error: ${error.message}`);
|
||||
failed += 1;
|
||||
}
|
||||
}
|
||||
|
||||
function createTempDir() {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-instinct-cli-projects-'));
|
||||
}
|
||||
|
||||
function cleanupDir(dir) {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
function writeJson(filePath, payload) {
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
fs.writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`);
|
||||
}
|
||||
|
||||
function readJson(filePath) {
|
||||
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||
}
|
||||
|
||||
function writeInstinct(filePath, id, confidence = 0.9) {
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
filePath,
|
||||
[
|
||||
'---',
|
||||
`id: ${id}`,
|
||||
'trigger: "when repeated"',
|
||||
`confidence: ${confidence}`,
|
||||
'domain: workflow',
|
||||
'---',
|
||||
'',
|
||||
`Action for ${id}.`,
|
||||
'',
|
||||
].join('\n')
|
||||
);
|
||||
}
|
||||
|
||||
function seedProject(root, id, options = {}) {
|
||||
const projectDir = path.join(root, 'projects', id);
|
||||
const personalDir = path.join(projectDir, 'instincts', 'personal');
|
||||
const inheritedDir = path.join(projectDir, 'instincts', 'inherited');
|
||||
fs.mkdirSync(personalDir, { recursive: true });
|
||||
fs.mkdirSync(inheritedDir, { recursive: true });
|
||||
|
||||
for (const instinct of options.personal || []) {
|
||||
writeInstinct(path.join(personalDir, `${instinct}.yaml`), instinct);
|
||||
}
|
||||
for (const instinct of options.inherited || []) {
|
||||
writeInstinct(path.join(inheritedDir, `${instinct}.yaml`), instinct);
|
||||
}
|
||||
if (options.observations) {
|
||||
fs.writeFileSync(
|
||||
path.join(projectDir, 'observations.jsonl'),
|
||||
options.observations.map(row => JSON.stringify(row)).join('\n') + '\n'
|
||||
);
|
||||
}
|
||||
|
||||
return projectDir;
|
||||
}
|
||||
|
||||
function projectHash(value) {
|
||||
return crypto.createHash('sha256').update(value).digest('hex').slice(0, 12);
|
||||
}
|
||||
|
||||
function runGit(cwd, args) {
|
||||
const result = spawnSync('git', args, {
|
||||
cwd,
|
||||
encoding: 'utf8',
|
||||
});
|
||||
assert.strictEqual(result.status, 0, result.stderr);
|
||||
return result.stdout.trim();
|
||||
}
|
||||
|
||||
function runCli(root, args, options = {}) {
|
||||
return spawnSync('python3', [cliPath, ...args], {
|
||||
cwd: options.cwd || repoRoot,
|
||||
encoding: 'utf8',
|
||||
env: {
|
||||
...process.env,
|
||||
CLV2_HOMUNCULUS_DIR: root,
|
||||
HOME: path.join(root, 'home'),
|
||||
USERPROFILE: path.join(root, 'home'),
|
||||
CLAUDE_PROJECT_DIR: '',
|
||||
...(options.env || {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log('\n=== Testing instinct-cli.py projects maintenance ===\n');
|
||||
|
||||
test('projects delete --dry-run preserves registry and project files', () => {
|
||||
const root = createTempDir();
|
||||
try {
|
||||
const registryPath = path.join(root, 'projects.json');
|
||||
seedProject(root, 'alpha123', {
|
||||
personal: ['keep-me'],
|
||||
observations: [{ event: 'tool_complete' }],
|
||||
});
|
||||
writeJson(registryPath, {
|
||||
alpha123: { name: 'alpha', root: '/repo/alpha', remote: '', last_seen: '2026-01-01T00:00:00Z' },
|
||||
});
|
||||
|
||||
const result = runCli(root, ['projects', 'delete', 'alpha123', '--dry-run']);
|
||||
assert.strictEqual(result.status, 0, result.stderr);
|
||||
assert.match(result.stdout, /would delete/i);
|
||||
assert.ok(fs.existsSync(path.join(root, 'projects', 'alpha123')));
|
||||
assert.ok(readJson(registryPath).alpha123);
|
||||
} finally {
|
||||
cleanupDir(root);
|
||||
}
|
||||
});
|
||||
|
||||
test('projects delete --force removes registry entry and project directory', () => {
|
||||
const root = createTempDir();
|
||||
try {
|
||||
const registryPath = path.join(root, 'projects.json');
|
||||
seedProject(root, 'alpha123', { personal: ['delete-me'] });
|
||||
writeJson(registryPath, {
|
||||
alpha123: { name: 'alpha', root: '/repo/alpha', remote: '', last_seen: '2026-01-01T00:00:00Z' },
|
||||
});
|
||||
|
||||
const result = runCli(root, ['projects', 'delete', 'alpha123', '--force']);
|
||||
assert.strictEqual(result.status, 0, result.stderr);
|
||||
assert.ok(!fs.existsSync(path.join(root, 'projects', 'alpha123')));
|
||||
assert.ok(!readJson(registryPath).alpha123);
|
||||
} finally {
|
||||
cleanupDir(root);
|
||||
}
|
||||
});
|
||||
|
||||
test('projects gc --force removes only zero-value project entries', () => {
|
||||
const root = createTempDir();
|
||||
try {
|
||||
const registryPath = path.join(root, 'projects.json');
|
||||
seedProject(root, 'empty000');
|
||||
seedProject(root, 'active999', { personal: ['active'] });
|
||||
writeJson(registryPath, {
|
||||
empty000: { name: 'empty', root: '/tmp/empty', remote: '', last_seen: '2026-01-01T00:00:00Z' },
|
||||
active999: { name: 'active', root: '/repo/active', remote: '', last_seen: '2026-01-02T00:00:00Z' },
|
||||
});
|
||||
|
||||
const result = runCli(root, ['projects', 'gc', '--force']);
|
||||
assert.strictEqual(result.status, 0, result.stderr);
|
||||
const registry = readJson(registryPath);
|
||||
assert.ok(!registry.empty000);
|
||||
assert.ok(registry.active999);
|
||||
assert.ok(!fs.existsSync(path.join(root, 'projects', 'empty000')));
|
||||
assert.ok(fs.existsSync(path.join(root, 'projects', 'active999')));
|
||||
} finally {
|
||||
cleanupDir(root);
|
||||
}
|
||||
});
|
||||
|
||||
test('projects merge deduplicates instincts, appends observations, and removes source', () => {
|
||||
const root = createTempDir();
|
||||
try {
|
||||
const registryPath = path.join(root, 'projects.json');
|
||||
seedProject(root, 'from111', {
|
||||
personal: ['shared', 'from-only'],
|
||||
observations: [{ event: 'from-event' }],
|
||||
});
|
||||
seedProject(root, 'into222', {
|
||||
personal: ['shared', 'into-only'],
|
||||
observations: [{ event: 'into-event' }],
|
||||
});
|
||||
writeJson(registryPath, {
|
||||
from111: { name: 'from', root: '/repo/from', remote: '', last_seen: '2026-01-01T00:00:00Z' },
|
||||
into222: { name: 'into', root: '/repo/into', remote: '', last_seen: '2026-01-02T00:00:00Z' },
|
||||
});
|
||||
|
||||
const result = runCli(root, ['projects', 'merge', 'from111', 'into222', '--force']);
|
||||
assert.strictEqual(result.status, 0, result.stderr);
|
||||
assert.ok(!fs.existsSync(path.join(root, 'projects', 'from111')));
|
||||
assert.ok(!readJson(registryPath).from111);
|
||||
assert.ok(readJson(registryPath).into222);
|
||||
|
||||
const intoPersonal = path.join(root, 'projects', 'into222', 'instincts', 'personal');
|
||||
assert.ok(fs.existsSync(path.join(intoPersonal, 'shared.yaml')));
|
||||
assert.ok(fs.existsSync(path.join(intoPersonal, 'from-only.yaml')));
|
||||
assert.ok(fs.existsSync(path.join(intoPersonal, 'into-only.yaml')));
|
||||
|
||||
const observations = fs.readFileSync(
|
||||
path.join(root, 'projects', 'into222', 'observations.jsonl'),
|
||||
'utf8'
|
||||
);
|
||||
assert.match(observations, /from-event/);
|
||||
assert.match(observations, /into-event/);
|
||||
} finally {
|
||||
cleanupDir(root);
|
||||
}
|
||||
});
|
||||
|
||||
test('status migrates legacy no-remote linked worktree project dirs to main worktree id', () => {
|
||||
const root = createTempDir();
|
||||
const repoParent = createTempDir();
|
||||
try {
|
||||
const mainWorktree = path.join(repoParent, 'main');
|
||||
const linkedWorktree = path.join(repoParent, 'linked');
|
||||
fs.mkdirSync(mainWorktree, { recursive: true });
|
||||
runGit(mainWorktree, ['init']);
|
||||
runGit(mainWorktree, ['config', 'user.email', 'ecc@example.test']);
|
||||
runGit(mainWorktree, ['config', 'user.name', 'ECC Test']);
|
||||
fs.writeFileSync(path.join(mainWorktree, 'README.md'), 'test\n');
|
||||
runGit(mainWorktree, ['add', 'README.md']);
|
||||
runGit(mainWorktree, ['commit', '-m', 'init']);
|
||||
runGit(mainWorktree, ['worktree', 'add', linkedWorktree]);
|
||||
|
||||
const mainRoot = runGit(mainWorktree, ['rev-parse', '--show-toplevel']);
|
||||
const linkedRoot = runGit(linkedWorktree, ['rev-parse', '--show-toplevel']);
|
||||
const oldLinkedId = projectHash(linkedRoot);
|
||||
const mainId = projectHash(mainRoot);
|
||||
seedProject(root, oldLinkedId, { personal: ['legacy-worktree'] });
|
||||
|
||||
const result = runCli(root, ['status'], { cwd: linkedRoot });
|
||||
assert.strictEqual(result.status, 0, result.stderr);
|
||||
assert.ok(!fs.existsSync(path.join(root, 'projects', oldLinkedId)));
|
||||
assert.ok(fs.existsSync(path.join(root, 'projects', mainId)));
|
||||
assert.ok(
|
||||
fs.existsSync(path.join(root, 'projects', mainId, 'instincts', 'personal', 'legacy-worktree.yaml'))
|
||||
);
|
||||
assert.match(result.stdout, new RegExp(`\\(${mainId}\\)`));
|
||||
} finally {
|
||||
cleanupDir(root);
|
||||
cleanupDir(repoParent);
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`\nPassed: ${passed}`);
|
||||
console.log(`Failed: ${failed}`);
|
||||
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
32
yarn.lock
32
yarn.lock
@@ -302,12 +302,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/node@npm:25.7.0":
|
||||
version: 25.7.0
|
||||
resolution: "@types/node@npm:25.7.0"
|
||||
"@types/node@npm:^25.8.0":
|
||||
version: 25.8.0
|
||||
resolution: "@types/node@npm:25.8.0"
|
||||
dependencies:
|
||||
undici-types: "npm:~7.21.0"
|
||||
checksum: 10c0/47ec7eaca154c36ad6d1ac0270e6e254eedf20b9dc49afe3bc76e4f7eba29ceac705f8903b162aeaf40e3941101ffe76ffb374989359ea3ef8c8509d8b443f55
|
||||
undici-types: "npm:>=7.24.0 <7.24.7"
|
||||
checksum: 10c0/ff53e5428309d2e6060190ec5e02afd0e4a7369456b16130a7f5898f12a6ad0efd62d752830f2f7355d714ae429bc0acbb2dc0cbf761cadb03e88c4996cdf1dc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -412,21 +412,21 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"brace-expansion@npm:^1.1.7":
|
||||
version: 1.1.14
|
||||
resolution: "brace-expansion@npm:1.1.14"
|
||||
version: 1.1.13
|
||||
resolution: "brace-expansion@npm:1.1.13"
|
||||
dependencies:
|
||||
balanced-match: "npm:^1.0.0"
|
||||
concat-map: "npm:0.0.1"
|
||||
checksum: 10c0/b6fdac832bc4e36a753658c9ed052c2e1a2be221763b002df25d1efbf7d21724334e726a6cd5eadc72a4b19ec3efb632d629cc003bc9c62f7af7a7915ffa4385
|
||||
checksum: 10c0/384c61bb329b6adfdcc0cbbdd108dc19fb5f3e84ae15a02a74f94c6c791b5a9b035aae73b2a51929a8a478e2f0f212a771eb6a8b5b514cccfb8d0c9f2ce8cbd8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"brace-expansion@npm:^5.0.5":
|
||||
version: 5.0.6
|
||||
resolution: "brace-expansion@npm:5.0.6"
|
||||
version: 5.0.5
|
||||
resolution: "brace-expansion@npm:5.0.5"
|
||||
dependencies:
|
||||
balanced-match: "npm:^4.0.2"
|
||||
checksum: 10c0/8c919869b90f61d533b341d3340be5ee4413232ea89b8246cbc2f38eb014f1d8182785c98a006eaf6111d02dc9eeffefdc240d5ac158625b2ed084dccd4bbf9b
|
||||
checksum: 10c0/4d238e14ed4f5cc9c07285550a41cef23121ca08ba99fa9eb5b55b580dcb6bf868b8210aa10526bdc9f8dc97f33ca2a7259039c4cc131a93042beddb424c48e3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -632,7 +632,7 @@ __metadata:
|
||||
"@eslint/js": "npm:^9.39.2"
|
||||
"@iarna/toml": "npm:^2.2.5"
|
||||
"@opencode-ai/plugin": "npm:^1.0.0"
|
||||
"@types/node": "npm:25.7.0"
|
||||
"@types/node": "npm:^25.8.0"
|
||||
ajv: "npm:^8.18.0"
|
||||
c8: "npm:^11.0.0"
|
||||
eslint: "npm:^9.39.2"
|
||||
@@ -2116,10 +2116,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"undici-types@npm:~7.21.0":
|
||||
version: 7.21.0
|
||||
resolution: "undici-types@npm:7.21.0"
|
||||
checksum: 10c0/c3b4ae5f066c398acb1962505b56214ecd72843f7d7827fcc2df7a48a63d1639d3608c580ac09f836253d21fa7ba8f1a04440569ed9d332474ad01b8a010db87
|
||||
"undici-types@npm:>=7.24.0 <7.24.7":
|
||||
version: 7.24.6
|
||||
resolution: "undici-types@npm:7.24.6"
|
||||
checksum: 10c0/d9cd8befb643ac904615c280a095ba4240531f6bb4a5e75a22a7483630ca8d3f1016d2ab6ace6ceda1f63b3a2db2fe037fafe121d6917a0187573aa548ff78ca
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user