Compare commits

..

3 Commits

Author SHA1 Message Date
Affaan Mustafa 958532920f fix(install): allow claude-project manifest target 2026-05-19 12:12:10 -04:00
Mhd Ghaith Al Abtah f4ff831890 test(install-targets): add positive rules assertion to claude-project foreign-path test
Addresses CodeRabbit review: the negative-only assertions could have
passed on an empty plan. Add a positive assertion that the non-foreign
'rules' path is still planned under .claude/rules/ecc so regression to
zero ops would fail loudly.
2026-05-19 19:50:48 +04:00
Mhd Ghaith Al Abtah b217bbc3fa feat(install-targets): add claude-project (per-project Claude Code) adapter
Completes the install-target matrix for Claude Code. Until now, ECC's
Claude support was home-scope only (~/.claude/) via the `claude` target.
This adds a project-scope counterpart (./.claude/) via a new
`claude-project` target so teams can install ECC per-repo without
contaminating ~/.claude/ — matching the existing project-scope adapters
for Cursor, Antigravity, Gemini, CodeBuddy, Joycode, and Zed.

Symmetric with `claude`:
- Same namespace under rules/ecc and skills/ecc
- Same docs/<locale> handling for --locale
- Same hooks placeholder substitution for hooks.json
- Reuses claude-home's destination-mapping logic 1:1

Use cases:
- Monorepos with multiple Flow-managed projects
- Teams that want ECC scoped per-project without touching ~/.claude/
- Per-project skill/rule isolation when global install isn't desirable

No breaking change: existing --target claude continues to route to
claude-home (user-scope) unchanged. New target is opt-in.

Tests
-----
- 4 new tests in tests/lib/install-targets.test.js
  (root resolution, lookup-by-id, plan parity with claude, foreign-path filtering)
- All install-target regression guards (schema enum / SUPPORTED_INSTALL_TARGETS)
  still pass
- End-to-end smoke: `--target claude-project --profile minimal --dry-run`
  emits 359 ops with destinations rooted at <projectRoot>/.claude/ (parity
  with --target claude which emits 359 ops rooted at ~/.claude/)
2026-05-19 19:50:25 +04:00
16 changed files with 115 additions and 858 deletions
+5 -8
View File
@@ -87,13 +87,10 @@ As of 2026-05-19:
(`owner-approval-packet-2026-05-19.md`), preview-pack smoke digest (`owner-approval-packet-2026-05-19.md`), preview-pack smoke digest
`790430aef4a8`, local 2550-test suite, PR #2001 merge and GitHub Actions run `790430aef4a8`, local 2550-test suite, PR #2001 merge and GitHub Actions run
`26102500291` success, PR #2002's owner-approval dashboard gate refresh and `26102500291` success, PR #2002's owner-approval dashboard gate refresh and
GitHub Actions run `26103853507`, PR #2004's Linear readiness evidence sync GitHub Actions run `26103853507`, plus PR #2004's Linear readiness evidence
and GitHub Actions run `26105012698`, plus PR #2005's post-PR #2004 sync and GitHub Actions run `26105012698`. The May 19 Linear sync document
evidence refresh and GitHub Actions run `26106321921`. The May 19 Linear remains the current external project status surface, and the May 18 evidence
sync document remains the current external project status surface, and the remains the detailed supply-chain and publication-path snapshot.
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.
- `docs/releases/2.0.0-rc.1/operator-readiness-dashboard-2026-05-19.md` - `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 regenerates the ITO-44 prompt-to-artifact dashboard from live platform audit
evidence: PR queue, issue queue, discussion queue, local worktree gate, 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 finding evidence paths, ECC-Tools #78 harness policy-route linking, PR #1947
supply-chain protection, and May 16 release-evidence supply-chain protection, and May 16 release-evidence
refresh. 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`, - `npm run observability:ready` reports 21/21 readiness on current `main`,
including the GitHub/Linear/handoff/roadmap progress-sync contract. including the GitHub/Linear/handoff/roadmap progress-sync contract.
- GitHub CI run `26017368895` completed successfully for - GitHub CI run `26017368895` completed successfully for
@@ -8,9 +8,9 @@ social announcement.
| Field | Evidence | | Field | Evidence |
| --- | --- | | --- | --- |
| Upstream main | `d6022d6b8dc5ef1393cf18ae40ee58f646f3754e` | | Upstream main | `ac7434ea8f39166b11e9d06ce64b38c4fb8d9202` |
| Git remote | `https://github.com/affaan-m/ECC.git` | | 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 | | 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 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 #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 #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 #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 ## 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 | | 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 | | 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 | | 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 |
| 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` |
| 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 | | 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 | | 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 | | 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 #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 #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 #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 | | 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 | | 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 | | 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 The tracked public PR queue, issue queue, discussion queue, canonical ECC
identity, release video suite, preview pack, and growth outreach packet are identity, release video suite, preview pack, and growth outreach packet are
current on May 19, 2026 for `main` through 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. owner approval, upload, and public URL attachment, not render or QA production.
This improves publication readiness but does not replace the approval-gated 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 | | 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 | | 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 | | 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 | Current release gate: PASS, 11 adapters | | Adapter scorecard | `npm run harness:adapters -- --check` | PASS | `99e01ded`: PASS, 11 adapters |
| Observability readiness | `npm run observability:ready` | 21/21 passing | Current release gate: 21/21, ready true | | 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 | Current release gate keeps Release Safety passing at 3/3; repeat the JSON gate from the exact final release commit | | 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 --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 | | 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 | PR #2005 CI `26106321921` completed successfully with 37/37 jobs and 0 failures; current branch reruns focused release/package/docs gates before merge | | 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 | Current release gate: focused lint passed for `publication-readiness.md`, `publication-evidence-2026-05-19.md`, and `docs/ECC-2.0-GA-ROADMAP.md` | | 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 | Current release gate: 2/2 passed | | 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 | Current release gate: 27/27 passed after refreshing the discussion-count assertion to the post-PR #2005 baseline | | 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 | | 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 | | 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 | 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 | | 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 | | 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 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 | | 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 |
+20 -20
View File
@@ -21,7 +21,7 @@
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.39.2", "@eslint/js": "^9.39.2",
"@opencode-ai/plugin": "^1.0.0", "@opencode-ai/plugin": "^1.0.0",
"@types/node": "25.7.0", "@types/node": "^25.8.0",
"c8": "^11.0.0", "c8": "^11.0.0",
"eslint": "^9.39.2", "eslint": "^9.39.2",
"globals": "^17.4.0", "globals": "^17.4.0",
@@ -398,13 +398,13 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "25.7.0", "version": "25.8.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.7.0.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.8.0.tgz",
"integrity": "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg==", "integrity": "sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"undici-types": "~7.21.0" "undici-types": ">=7.24.0 <7.24.7"
} }
}, },
"node_modules/@types/unist": { "node_modules/@types/unist": {
@@ -497,9 +497,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "1.1.14", "version": "1.1.13",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
"integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -1210,9 +1210,9 @@
} }
}, },
"node_modules/glob/node_modules/brace-expansion": { "node_modules/glob/node_modules/brace-expansion": {
"version": "5.0.6", "version": "5.0.5",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -1680,9 +1680,9 @@
} }
}, },
"node_modules/markdownlint-cli/node_modules/brace-expansion": { "node_modules/markdownlint-cli/node_modules/brace-expansion": {
"version": "5.0.6", "version": "5.0.5",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -2666,9 +2666,9 @@
} }
}, },
"node_modules/test-exclude/node_modules/brace-expansion": { "node_modules/test-exclude/node_modules/brace-expansion": {
"version": "5.0.6", "version": "5.0.5",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -2745,9 +2745,9 @@
"dev": true "dev": true
}, },
"node_modules/undici-types": { "node_modules/undici-types": {
"version": "7.21.0", "version": "7.24.6",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.21.0.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
"integrity": "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ==", "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
+1 -1
View File
@@ -335,7 +335,7 @@
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.39.2", "@eslint/js": "^9.39.2",
"@opencode-ai/plugin": "^1.0.0", "@opencode-ai/plugin": "^1.0.0",
"@types/node": "25.7.0", "@types/node": "^25.8.0",
"c8": "^11.0.0", "c8": "^11.0.0",
"eslint": "^9.39.2", "eslint": "^9.39.2",
"globals": "^17.4.0", "globals": "^17.4.0",
+3 -70
View File
@@ -119,65 +119,6 @@ function tokenize(segment) {
return segment.split(/\s+/).filter(Boolean); 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 * Strip a leading path and trailing `.exe` from a command token so
* `/usr/bin/git`, `git.exe`, and `GIT` all normalize to `git`. * `/usr/bin/git`, `git.exe`, and `GIT` all normalize to `git`.
@@ -651,16 +592,8 @@ function isReadOnlyGitIntrospection(command) {
return false; return false;
} }
const segments = splitCommandSegments(trimmed); const tokens = trimmed.split(/\s+/);
if (segments.length !== 1) { if (tokens[0] !== 'git' || tokens.length < 2) {
return false;
}
const tokens = tokenizeAllowlistedShellWords(trimmed);
if (!tokens) {
return false;
}
if (commandBasename(tokens[0]) !== 'git' || tokens.length < 2) {
return false; return false;
} }
@@ -680,7 +613,7 @@ function isReadOnlyGitIntrospection(command) {
} }
if (subcommand === 'show') { 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') { if (subcommand === 'branch') {
@@ -116,13 +116,7 @@ except(KeyError, TypeError, ValueError):
# If cwd was provided in stdin, use it for project detection # If cwd was provided in stdin, use it for project detection
if [ -n "$STDIN_CWD" ] && [ -d "$STDIN_CWD" ]; then if [ -n "$STDIN_CWD" ] && [ -d "$STDIN_CWD" ]; then
_GIT_ROOT=$(git -C "$STDIN_CWD" rev-parse --show-toplevel 2>/dev/null || true) _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:-$STDIN_CWD}"
export CLAUDE_PROJECT_DIR="$_GIT_ROOT"
unset CLV2_NO_PROJECT
else
unset CLAUDE_PROJECT_DIR
export CLV2_NO_PROJECT=1
fi
fi fi
# ───────────────────────────────────────────── # ─────────────────────────────────────────────
@@ -75,42 +75,16 @@ _clv2_normalize_remote_url() {
fi 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() { _clv2_detect_project() {
local project_root="" local project_root=""
local project_name="" local project_name=""
local project_id="" local project_id=""
local source_hint="" 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 # 1. Try CLAUDE_PROJECT_DIR env var
if [ -n "$CLAUDE_PROJECT_DIR" ] && [ -d "$CLAUDE_PROJECT_DIR" ] && command -v git &>/dev/null; then if [ -n "$CLAUDE_PROJECT_DIR" ] && [ -d "$CLAUDE_PROJECT_DIR" ]; then
project_root=$(git -C "$CLAUDE_PROJECT_DIR" rev-parse --show-toplevel 2>/dev/null || true) project_root="$CLAUDE_PROJECT_DIR"
if [ -n "$project_root" ]; then source_hint="env"
source_hint="env"
fi
fi fi
# 2. Try git repo root from CWD (only if git is available) # 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_NAME="global"
_CLV2_PROJECT_ROOT="" _CLV2_PROJECT_ROOT=""
_CLV2_PROJECT_DIR="${_CLV2_HOMUNCULUS_DIR}" _CLV2_PROJECT_DIR="${_CLV2_HOMUNCULUS_DIR}"
mkdir -p "$_CLV2_PROJECT_DIR"
return 0 return 0
fi fi
@@ -160,14 +133,7 @@ _clv2_detect_project() {
normalized_remote=$(_clv2_normalize_remote_url "$remote_url") normalized_remote=$(_clv2_normalize_remote_url "$remote_url")
fi fi
local fallback_root="$project_root" local hash_input="${normalized_remote:-${remote_url:-$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}}"
# Prefer Python for consistent SHA256 behavior across shells/platforms. # 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 # 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 # is locale-independent (shells vary between UTF-8 / CP932 / CP1252, which
@@ -22,7 +22,6 @@ import os
import subprocess import subprocess
import sys import sys
import re import re
import shutil
import urllib.request import urllib.request
from pathlib import Path from pathlib import Path
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
@@ -195,64 +194,26 @@ def _yaml_quote(value: str) -> str:
# Project Detection (Python equivalent of detect-project.sh) # 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: def detect_project() -> dict:
"""Detect current project context. Returns dict with id, name, root, project_dir.""" """Detect current project context. Returns dict with id, name, root, project_dir."""
project_root = None 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 # 1. CLAUDE_PROJECT_DIR env var
env_dir = os.environ.get("CLAUDE_PROJECT_DIR") env_dir = os.environ.get("CLAUDE_PROJECT_DIR")
if env_dir and os.path.isdir(env_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 # 2. git repo root
if not project_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 # Normalize: strip trailing slashes to keep basename and hash stable
if project_root: if project_root:
@@ -289,10 +250,9 @@ def detect_project() -> dict:
if remote_url: if remote_url:
remote_url = _strip_remote_credentials(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 legacy_hash_source = remote_url if remote_url else project_root
normalized_remote = _normalize_remote_url(remote_url) if remote_url else "" 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_id = _project_hash(hash_source)
project_dir = PROJECTS_DIR / project_id project_dir = PROJECTS_DIR / project_id
@@ -392,26 +352,6 @@ def load_registry() -> dict:
return {} 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 # Instinct Parser
# ───────────────────────────────────────────── # ─────────────────────────────────────────────
@@ -496,96 +436,6 @@ def _load_instincts_from_dir(directory: Path, source_type: str, scope_label: str
return instincts 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]: def load_all_instincts(project: dict, include_global: bool = True) -> list[dict]:
"""Load all instincts: project-scoped + global. """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: def cmd_projects(args) -> int:
"""List or maintain known projects and their instinct counts.""" """List all 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)
registry = load_registry() registry = load_registry()
if not registry: if not registry:
@@ -1382,143 +1225,6 @@ def cmd_projects(args) -> int:
return 0 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 # Generate Evolved Structures
# ───────────────────────────────────────────── # ─────────────────────────────────────────────
@@ -1780,19 +1486,6 @@ def main() -> int:
# Projects (new in v2.1) # Projects (new in v2.1)
projects_parser = subparsers.add_parser('projects', help='List known projects and instinct counts') 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 (pending instinct TTL)
prune_parser = subparsers.add_parser('prune', help='Delete pending instincts older than TTL') prune_parser = subparsers.add_parser('prune', help='Delete pending instincts older than TTL')
+1 -1
View File
@@ -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-plugin-publication-checklist-2026-05-18.md'));
assert.ok(source.includes('Release name and plugin publication checklist')); 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(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('0 needing maintainer touch'));
assert.ok(source.includes('discussion-response-playbook.md')); assert.ok(source.includes('discussion-response-playbook.md'));
for (const expected of [ for (const expected of [
+22 -62
View File
@@ -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'); const worktreeDir = path.join(testDir, 'my-worktree');
execSync(`git worktree add "${worktreeDir}" -b feature/project-id`, { fs.mkdirSync(worktreeDir, { recursive: true });
cwd: mainRepo,
stdio: 'pipe' // Set up the worktree directory structure in the main repo
}); const worktreesDir = path.join(mainRepo, '.git', 'worktrees', 'my-worktree');
assert.ok( fs.mkdirSync(worktreesDir, { recursive: true });
fs.statSync(path.join(worktreeDir, '.git')).isFile(),
'linked worktree should expose .git as a file' // 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 // 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 // Summary
// ────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────
+1 -16
View File
@@ -769,8 +769,6 @@ function runTests() {
'git diff --name-only', 'git diff --name-only',
'git log --oneline --max-count=1', 'git log --oneline --max-count=1',
'git show HEAD:README.md', 'git show HEAD:README.md',
'git show HEAD:"docs/install guide.md"',
'/usr/bin/git status --short',
'git branch --show-current', 'git branch --show-current',
'git rev-parse --abbrev-ref HEAD', 'git rev-parse --abbrev-ref HEAD',
]; ];
@@ -804,20 +802,7 @@ function runTests() {
assert.ok(output.hookSpecificOutput.permissionDecisionReason.includes('current user request')); assert.ok(output.hookSpecificOutput.permissionDecisionReason.includes('current user request'));
})) passed++; else failed++; })) passed++; else failed++;
// --- Test 23: quoted shell separators are not read-only git bypasses // --- Test 23: module-load pruning removes old state files only ---
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 ---
clearState(); clearState();
if (test('prunes stale state files while keeping fresh state files', () => { if (test('prunes stale state files while keeping fresh state files', () => {
const staleFile = path.join(stateDir, 'state-stale-session.json'); const staleFile = path.join(stateDir, 'state-stale-session.json');
+4 -7
View File
@@ -3300,14 +3300,11 @@ async function runTests() {
assert.strictEqual(result.code, 0, `observe.sh should exit successfully, stderr: ${result.stderr}`); 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(homeDir, '.local', 'share', 'ecc-homunculus', 'projects');
const projectsDir = path.join(homunculusDir, 'projects'); const projectIds = fs.readdirSync(projectsDir);
assert.ok( assert.strictEqual(projectIds.length, 1, 'observe.sh should create one project-scoped observation directory');
!fs.existsSync(projectsDir) || fs.readdirSync(projectsDir).length === 0,
'observe.sh should not create a project-scoped directory for a non-git cwd'
);
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); const observations = fs.readFileSync(observationsPath, 'utf8').trim().split('\n').filter(Boolean);
assert.ok(observations.length > 0, 'observe.sh should append at least one observation'); 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' 'observe.sh should resolve STDIN_CWD to git repo root'
); );
assert.ok( assert.ok(
content.includes('export CLV2_NO_PROJECT=1'), content.includes('${_GIT_ROOT:-$STDIN_CWD}'),
'observe.sh should mark non-git cwd payloads as global instead of registering raw 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(); const testRoot = createTempDir();
try { try {
@@ -262,17 +262,12 @@ test('observe.sh records non-git cwd payloads globally without project registry
const result = runObserve({ homeDir, cwd: nonGitDir }); const result = runObserve({ homeDir, cwd: nonGitDir });
assert.strictEqual(result.status, 0, result.stderr); assert.strictEqual(result.status, 0, result.stderr);
const homunculusDir = path.join(homeDir, '.local', 'share', 'ecc-homunculus'); const { metadata } = readSingleProjectMetadata(homeDir);
const projectsDir = path.join(homunculusDir, 'projects'); assert.strictEqual(
const registryPath = path.join(homunculusDir, 'projects.json'); normalizeComparablePath(metadata.root),
const observationsPath = path.join(homunculusDir, 'observations.jsonl'); normalizeComparablePath(nonGitDir),
'project metadata root should stay on the non-git cwd'
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'
); );
assert.ok(fs.existsSync(observationsPath), 'non-git cwd should still record a global observation');
} finally { } finally {
cleanupDir(testRoot); cleanupDir(testRoot);
} }
-260
View File
@@ -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);
+16 -16
View File
@@ -302,12 +302,12 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/node@npm:25.7.0": "@types/node@npm:^25.8.0":
version: 25.7.0 version: 25.8.0
resolution: "@types/node@npm:25.7.0" resolution: "@types/node@npm:25.8.0"
dependencies: dependencies:
undici-types: "npm:~7.21.0" undici-types: "npm:>=7.24.0 <7.24.7"
checksum: 10c0/47ec7eaca154c36ad6d1ac0270e6e254eedf20b9dc49afe3bc76e4f7eba29ceac705f8903b162aeaf40e3941101ffe76ffb374989359ea3ef8c8509d8b443f55 checksum: 10c0/ff53e5428309d2e6060190ec5e02afd0e4a7369456b16130a7f5898f12a6ad0efd62d752830f2f7355d714ae429bc0acbb2dc0cbf761cadb03e88c4996cdf1dc
languageName: node languageName: node
linkType: hard linkType: hard
@@ -412,21 +412,21 @@ __metadata:
linkType: hard linkType: hard
"brace-expansion@npm:^1.1.7": "brace-expansion@npm:^1.1.7":
version: 1.1.14 version: 1.1.13
resolution: "brace-expansion@npm:1.1.14" resolution: "brace-expansion@npm:1.1.13"
dependencies: dependencies:
balanced-match: "npm:^1.0.0" balanced-match: "npm:^1.0.0"
concat-map: "npm:0.0.1" concat-map: "npm:0.0.1"
checksum: 10c0/b6fdac832bc4e36a753658c9ed052c2e1a2be221763b002df25d1efbf7d21724334e726a6cd5eadc72a4b19ec3efb632d629cc003bc9c62f7af7a7915ffa4385 checksum: 10c0/384c61bb329b6adfdcc0cbbdd108dc19fb5f3e84ae15a02a74f94c6c791b5a9b035aae73b2a51929a8a478e2f0f212a771eb6a8b5b514cccfb8d0c9f2ce8cbd8
languageName: node languageName: node
linkType: hard linkType: hard
"brace-expansion@npm:^5.0.5": "brace-expansion@npm:^5.0.5":
version: 5.0.6 version: 5.0.5
resolution: "brace-expansion@npm:5.0.6" resolution: "brace-expansion@npm:5.0.5"
dependencies: dependencies:
balanced-match: "npm:^4.0.2" balanced-match: "npm:^4.0.2"
checksum: 10c0/8c919869b90f61d533b341d3340be5ee4413232ea89b8246cbc2f38eb014f1d8182785c98a006eaf6111d02dc9eeffefdc240d5ac158625b2ed084dccd4bbf9b checksum: 10c0/4d238e14ed4f5cc9c07285550a41cef23121ca08ba99fa9eb5b55b580dcb6bf868b8210aa10526bdc9f8dc97f33ca2a7259039c4cc131a93042beddb424c48e3
languageName: node languageName: node
linkType: hard linkType: hard
@@ -632,7 +632,7 @@ __metadata:
"@eslint/js": "npm:^9.39.2" "@eslint/js": "npm:^9.39.2"
"@iarna/toml": "npm:^2.2.5" "@iarna/toml": "npm:^2.2.5"
"@opencode-ai/plugin": "npm:^1.0.0" "@opencode-ai/plugin": "npm:^1.0.0"
"@types/node": "npm:25.7.0" "@types/node": "npm:^25.8.0"
ajv: "npm:^8.18.0" ajv: "npm:^8.18.0"
c8: "npm:^11.0.0" c8: "npm:^11.0.0"
eslint: "npm:^9.39.2" eslint: "npm:^9.39.2"
@@ -2116,10 +2116,10 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"undici-types@npm:~7.21.0": "undici-types@npm:>=7.24.0 <7.24.7":
version: 7.21.0 version: 7.24.6
resolution: "undici-types@npm:7.21.0" resolution: "undici-types@npm:7.24.6"
checksum: 10c0/c3b4ae5f066c398acb1962505b56214ecd72843f7d7827fcc2df7a48a63d1639d3608c580ac09f836253d21fa7ba8f1a04440569ed9d332474ad01b8a010db87 checksum: 10c0/d9cd8befb643ac904615c280a095ba4240531f6bb4a5e75a22a7483630ca8d3f1016d2ab6ace6ceda1f63b3a2db2fe037fafe121d6917a0187573aa548ff78ca
languageName: node languageName: node
linkType: hard linkType: hard