peaks-loop 4.1.2 → 4.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +136 -47
- package/CHANGELOG.md +56 -0
- package/README-en.md +1 -1
- package/README.md +1 -1
- package/contracts/readonly-argv-whitelist.json +211 -0
- package/dist/cli/commands/audit-artifact-runners.d.ts +16 -0
- package/dist/cli/commands/audit-artifact-runners.js +170 -0
- package/dist/cli/commands/audit-command-shared.d.ts +97 -0
- package/dist/cli/commands/audit-command-shared.js +60 -0
- package/dist/cli/commands/audit-commands.d.ts +7 -31
- package/dist/cli/commands/audit-commands.js +64 -473
- package/dist/cli/commands/audit-goal-runners.d.ts +17 -0
- package/dist/cli/commands/audit-goal-runners.js +114 -0
- package/dist/cli/commands/audit-red-lines-runners.d.ts +11 -0
- package/dist/cli/commands/audit-red-lines-runners.js +43 -0
- package/dist/cli/commands/audit-refusal-data.d.ts +23 -0
- package/dist/cli/commands/audit-refusal-data.js +69 -0
- package/dist/cli/commands/audit-static-runners.d.ts +13 -0
- package/dist/cli/commands/audit-static-runners.js +139 -0
- package/dist/cli/commands/await-runners.d.ts +11 -0
- package/dist/cli/commands/await-runners.js +148 -0
- package/dist/cli/commands/code-auto-compact-command.d.ts +3 -0
- package/dist/cli/commands/code-auto-compact-command.js +124 -0
- package/dist/cli/commands/code-auto-compact-envelope.d.ts +22 -0
- package/dist/cli/commands/code-auto-compact-envelope.js +39 -0
- package/dist/cli/commands/code-context-audit-command.d.ts +3 -0
- package/dist/cli/commands/code-context-audit-command.js +68 -0
- package/dist/cli/commands/code-context-now-command.d.ts +3 -0
- package/dist/cli/commands/code-context-now-command.js +124 -0
- package/dist/cli/commands/code-context-now-envelope.d.ts +66 -0
- package/dist/cli/commands/code-context-now-envelope.js +136 -0
- package/dist/cli/commands/code-context-now-mapping.d.ts +27 -0
- package/dist/cli/commands/code-context-now-mapping.js +43 -0
- package/dist/cli/commands/code-emit-handoff-command.d.ts +3 -0
- package/dist/cli/commands/code-emit-handoff-command.js +99 -0
- package/dist/cli/commands/code-gate-step-08-command.d.ts +3 -0
- package/dist/cli/commands/code-gate-step-08-command.js +139 -0
- package/dist/cli/commands/code-post-compact-detect-command.d.ts +3 -0
- package/dist/cli/commands/code-post-compact-detect-command.js +50 -0
- package/dist/cli/commands/code-runtime-commands.d.ts +9 -43
- package/dist/cli/commands/code-runtime-commands.js +20 -672
- package/dist/cli/commands/code-runtime-session.d.ts +1 -0
- package/dist/cli/commands/code-runtime-session.js +15 -0
- package/dist/cli/commands/compact-command.d.ts +6 -2
- package/dist/cli/commands/compact-command.js +12 -559
- package/dist/cli/commands/compact-dry-run-command.d.ts +3 -0
- package/dist/cli/commands/compact-dry-run-command.js +87 -0
- package/dist/cli/commands/compact-force-command.d.ts +3 -0
- package/dist/cli/commands/compact-force-command.js +98 -0
- package/dist/cli/commands/compact-harness-window-command.d.ts +3 -0
- package/dist/cli/commands/compact-harness-window-command.js +174 -0
- package/dist/cli/commands/compact-history-command.d.ts +3 -0
- package/dist/cli/commands/compact-history-command.js +89 -0
- package/dist/cli/commands/compact-read-commands.d.ts +3 -0
- package/dist/cli/commands/compact-read-commands.js +123 -0
- package/dist/cli/commands/compact-settle-command.d.ts +3 -0
- package/dist/cli/commands/compact-settle-command.js +130 -0
- package/dist/cli/commands/compact-shared.d.ts +9 -0
- package/dist/cli/commands/compact-shared.js +26 -0
- package/dist/cli/commands/core/skill-command.d.ts +1 -44
- package/dist/cli/commands/core/skill-command.js +13 -541
- package/dist/cli/commands/core/skill-context-verdict.d.ts +38 -0
- package/dist/cli/commands/core/skill-context-verdict.js +63 -0
- package/dist/cli/commands/core/skill-generated-config-notice.d.ts +41 -0
- package/dist/cli/commands/core/skill-generated-config-notice.js +75 -0
- package/dist/cli/commands/core/skill-heartbeat-commands.d.ts +3 -0
- package/dist/cli/commands/core/skill-heartbeat-commands.js +50 -0
- package/dist/cli/commands/core/skill-presence-commands.d.ts +3 -0
- package/dist/cli/commands/core/skill-presence-commands.js +123 -0
- package/dist/cli/commands/core/skill-presence-housekeeping-commands.d.ts +3 -0
- package/dist/cli/commands/core/skill-presence-housekeeping-commands.js +165 -0
- package/dist/cli/commands/core/skill-registry-commands.d.ts +3 -0
- package/dist/cli/commands/core/skill-registry-commands.js +117 -0
- package/dist/cli/commands/core/skill-runbook-command.d.ts +3 -0
- package/dist/cli/commands/core/skill-runbook-command.js +29 -0
- package/dist/cli/commands/dispatch-command-spec.d.ts +2 -0
- package/dist/cli/commands/dispatch-command-spec.js +62 -0
- package/dist/cli/commands/dispatch-commands.js +159 -775
- package/dist/cli/commands/dispatch-detached-branch.d.ts +3 -0
- package/dist/cli/commands/dispatch-detached-branch.js +58 -0
- package/dist/cli/commands/dispatch-envelope.d.ts +36 -0
- package/dist/cli/commands/dispatch-envelope.js +104 -0
- package/dist/cli/commands/dispatch-isolation-lease.d.ts +17 -0
- package/dist/cli/commands/dispatch-isolation-lease.js +149 -0
- package/dist/cli/commands/dispatch-preflight.d.ts +7 -0
- package/dist/cli/commands/dispatch-preflight.js +105 -0
- package/dist/cli/commands/dispatch-prompt-blocks.d.ts +10 -0
- package/dist/cli/commands/dispatch-prompt-blocks.js +56 -0
- package/dist/cli/commands/dispatch-prompt-composition.d.ts +30 -0
- package/dist/cli/commands/dispatch-prompt-composition.js +171 -0
- package/dist/cli/commands/dispatch-record-write.d.ts +24 -0
- package/dist/cli/commands/dispatch-record-write.js +96 -0
- package/dist/cli/commands/dispatch-tool-call.d.ts +15 -0
- package/dist/cli/commands/dispatch-tool-call.js +128 -0
- package/dist/cli/commands/evolution-command-shared.d.ts +78 -0
- package/dist/cli/commands/evolution-command-shared.js +107 -0
- package/dist/cli/commands/evolution-commands.d.ts +11 -1
- package/dist/cli/commands/evolution-commands.js +19 -350
- package/dist/cli/commands/evolution-evaluate-command.d.ts +24 -0
- package/dist/cli/commands/evolution-evaluate-command.js +138 -0
- package/dist/cli/commands/evolution-propose-command.d.ts +23 -0
- package/dist/cli/commands/evolution-propose-command.js +145 -0
- package/dist/cli/commands/evolution-status-command.d.ts +17 -0
- package/dist/cli/commands/evolution-status-command.js +41 -0
- package/dist/cli/commands/evolution-verdict-commands.d.ts +30 -0
- package/dist/cli/commands/evolution-verdict-commands.js +100 -0
- package/dist/cli/commands/finalize-accumulator.d.ts +44 -0
- package/dist/cli/commands/finalize-accumulator.js +55 -0
- package/dist/cli/commands/finalize-branches.d.ts +34 -0
- package/dist/cli/commands/finalize-branches.js +119 -0
- package/dist/cli/commands/finalize-runners.d.ts +3 -0
- package/dist/cli/commands/finalize-runners.js +92 -0
- package/dist/cli/commands/finalize-selection.d.ts +65 -0
- package/dist/cli/commands/finalize-selection.js +35 -0
- package/dist/cli/commands/gate-commands.js +26 -8
- package/dist/cli/commands/job-checkpoint-commands.d.ts +3 -0
- package/dist/cli/commands/job-checkpoint-commands.js +157 -0
- package/dist/cli/commands/job-command-shared.d.ts +109 -0
- package/dist/cli/commands/job-command-shared.js +72 -0
- package/dist/cli/commands/job-commands.d.ts +4 -64
- package/dist/cli/commands/job-commands.js +42 -552
- package/dist/cli/commands/job-cost-check-command.d.ts +3 -0
- package/dist/cli/commands/job-cost-check-command.js +41 -0
- package/dist/cli/commands/job-init-status-commands.d.ts +3 -0
- package/dist/cli/commands/job-init-status-commands.js +149 -0
- package/dist/cli/commands/job-run-commands.d.ts +3 -0
- package/dist/cli/commands/job-run-commands.js +97 -0
- package/dist/cli/commands/job-state-root.d.ts +35 -0
- package/dist/cli/commands/job-state-root.js +76 -0
- package/dist/cli/commands/loop-eval-capture-index.d.ts +4 -0
- package/dist/cli/commands/loop-eval-capture-index.js +44 -0
- package/dist/cli/commands/loop-eval-commands.d.ts +8 -5
- package/dist/cli/commands/loop-eval-commands.js +12 -626
- package/dist/cli/commands/loop-eval-eval-command.d.ts +3 -0
- package/dist/cli/commands/loop-eval-eval-command.js +168 -0
- package/dist/cli/commands/loop-eval-monotonic-command.d.ts +3 -0
- package/dist/cli/commands/loop-eval-monotonic-command.js +90 -0
- package/dist/cli/commands/loop-eval-parents.d.ts +7 -0
- package/dist/cli/commands/loop-eval-parents.js +20 -0
- package/dist/cli/commands/loop-eval-run-code.d.ts +7 -0
- package/dist/cli/commands/loop-eval-run-code.js +40 -0
- package/dist/cli/commands/loop-eval-run-command.d.ts +3 -0
- package/dist/cli/commands/loop-eval-run-command.js +113 -0
- package/dist/cli/commands/loop-eval-spec-commands.d.ts +3 -0
- package/dist/cli/commands/loop-eval-spec-commands.js +165 -0
- package/dist/cli/commands/loop-eval-spec-lint-command.d.ts +3 -0
- package/dist/cli/commands/loop-eval-spec-lint-command.js +42 -0
- package/dist/cli/commands/loop-eval-workflow-commands.d.ts +3 -0
- package/dist/cli/commands/loop-eval-workflow-commands.js +141 -0
- package/dist/cli/commands/mcp-commands.d.ts +10 -0
- package/dist/cli/commands/mcp-commands.js +157 -0
- package/dist/cli/commands/request-command-options.d.ts +49 -0
- package/dist/cli/commands/request-command-options.js +1 -0
- package/dist/cli/commands/request-commands.d.ts +3 -9
- package/dist/cli/commands/request-commands.js +17 -522
- package/dist/cli/commands/request-init-command.d.ts +3 -0
- package/dist/cli/commands/request-init-command.js +100 -0
- package/dist/cli/commands/request-lint-command.d.ts +3 -0
- package/dist/cli/commands/request-lint-command.js +50 -0
- package/dist/cli/commands/request-list-command.d.ts +10 -0
- package/dist/cli/commands/request-list-command.js +53 -0
- package/dist/cli/commands/request-repair-status-command.d.ts +3 -0
- package/dist/cli/commands/request-repair-status-command.js +61 -0
- package/dist/cli/commands/request-show-command.d.ts +3 -0
- package/dist/cli/commands/request-show-command.js +56 -0
- package/dist/cli/commands/request-transition-command.d.ts +3 -0
- package/dist/cli/commands/request-transition-command.js +122 -0
- package/dist/cli/commands/request-transition-failures.d.ts +8 -0
- package/dist/cli/commands/request-transition-failures.js +90 -0
- package/dist/cli/commands/request-transition-hooks.d.ts +50 -0
- package/dist/cli/commands/request-transition-hooks.js +115 -0
- package/dist/cli/commands/request-transition-preflight.d.ts +10 -0
- package/dist/cli/commands/request-transition-preflight.js +107 -0
- package/dist/cli/commands/scan-command-shared.d.ts +96 -0
- package/dist/cli/commands/scan-command-shared.js +83 -0
- package/dist/cli/commands/scan-commands.d.ts +3 -2
- package/dist/cli/commands/scan-commands.js +23 -463
- package/dist/cli/commands/scan-inventory-commands.d.ts +3 -0
- package/dist/cli/commands/scan-inventory-commands.js +96 -0
- package/dist/cli/commands/scan-project-commands.d.ts +3 -0
- package/dist/cli/commands/scan-project-commands.js +65 -0
- package/dist/cli/commands/scan-scope-commands.d.ts +3 -0
- package/dist/cli/commands/scan-scope-commands.js +149 -0
- package/dist/cli/commands/scan-scope-helpers.d.ts +8 -0
- package/dist/cli/commands/scan-scope-helpers.js +54 -0
- package/dist/cli/commands/scan-structural-commands.d.ts +3 -0
- package/dist/cli/commands/scan-structural-commands.js +113 -0
- package/dist/cli/commands/share-commands.d.ts +12 -68
- package/dist/cli/commands/share-commands.js +47 -556
- package/dist/cli/commands/share-error-hints.d.ts +14 -0
- package/dist/cli/commands/share-error-hints.js +38 -0
- package/dist/cli/commands/share-identity.d.ts +13 -0
- package/dist/cli/commands/share-identity.js +22 -0
- package/dist/cli/commands/share-record-paths.d.ts +37 -0
- package/dist/cli/commands/share-record-paths.js +68 -0
- package/dist/cli/commands/share-runners.d.ts +3 -0
- package/dist/cli/commands/share-runners.js +158 -0
- package/dist/cli/commands/shared-read-runners.d.ts +3 -0
- package/dist/cli/commands/shared-read-runners.js +51 -0
- package/dist/cli/commands/slice-check-command.d.ts +3 -0
- package/dist/cli/commands/slice-check-command.js +54 -0
- package/dist/cli/commands/slice-commands.d.ts +2 -22
- package/dist/cli/commands/slice-commands.js +18 -517
- package/dist/cli/commands/slice-decompose-artifacts.d.ts +18 -0
- package/dist/cli/commands/slice-decompose-artifacts.js +79 -0
- package/dist/cli/commands/slice-decompose-command.d.ts +3 -0
- package/dist/cli/commands/slice-decompose-command.js +123 -0
- package/dist/cli/commands/slice-list-commands.d.ts +3 -0
- package/dist/cli/commands/slice-list-commands.js +182 -0
- package/dist/cli/commands/slice-pick-command.d.ts +3 -0
- package/dist/cli/commands/slice-pick-command.js +60 -0
- package/dist/cli/commands/slice-plan-command.d.ts +24 -0
- package/dist/cli/commands/slice-plan-command.js +111 -0
- package/dist/cli/commands/workflow-commands.d.ts +10 -1
- package/dist/cli/commands/workflow-commands.js +6 -594
- package/dist/cli/commands/workflow-gate-commands.d.ts +3 -0
- package/dist/cli/commands/workflow-gate-commands.js +116 -0
- package/dist/cli/commands/workflow-plan-helpers.d.ts +48 -0
- package/dist/cli/commands/workflow-plan-helpers.js +103 -0
- package/dist/cli/commands/workflow-planning-commands.d.ts +3 -0
- package/dist/cli/commands/workflow-planning-commands.js +56 -0
- package/dist/cli/commands/workflow-swarm-actions.d.ts +7 -0
- package/dist/cli/commands/workflow-swarm-actions.js +109 -0
- package/dist/cli/commands/workflow-swarm-commands.d.ts +3 -0
- package/dist/cli/commands/workflow-swarm-commands.js +138 -0
- package/dist/cli/commands/workflow-tech-actions.d.ts +10 -0
- package/dist/cli/commands/workflow-tech-actions.js +150 -0
- package/dist/cli/commands/workspace/init-actions.d.ts +10 -0
- package/dist/cli/commands/workspace/init-actions.js +199 -0
- package/dist/cli/commands/workspace/init-codegraph-stake.d.ts +10 -0
- package/dist/cli/commands/workspace/init-codegraph-stake.js +54 -0
- package/dist/cli/commands/workspace/init-command.d.ts +8 -124
- package/dist/cli/commands/workspace/init-command.js +59 -623
- package/dist/cli/commands/workspace/init-context.d.ts +58 -0
- package/dist/cli/commands/workspace/init-context.js +121 -0
- package/dist/cli/commands/workspace/init-envelope.d.ts +3 -0
- package/dist/cli/commands/workspace/init-envelope.js +34 -0
- package/dist/cli/commands/workspace/init-failures.d.ts +3 -0
- package/dist/cli/commands/workspace/init-failures.js +84 -0
- package/dist/cli/commands/workspace/init-hooks-decision.d.ts +24 -0
- package/dist/cli/commands/workspace/init-hooks-decision.js +189 -0
- package/dist/cli/commands/workspace/init-options.d.ts +102 -0
- package/dist/cli/commands/workspace/init-options.js +1 -0
- package/dist/cli/commands/worktree-lease-commands.d.ts +10 -6
- package/dist/cli/commands/worktree-lease-commands.js +42 -512
- package/dist/cli/commands/worktree-lease-gc-command.d.ts +12 -0
- package/dist/cli/commands/worktree-lease-gc-command.js +117 -0
- package/dist/cli/commands/worktree-lease-git.d.ts +22 -0
- package/dist/cli/commands/worktree-lease-git.js +48 -0
- package/dist/cli/commands/worktree-lease-list-command.d.ts +12 -0
- package/dist/cli/commands/worktree-lease-list-command.js +100 -0
- package/dist/cli/commands/worktree-lease-load.d.ts +37 -0
- package/dist/cli/commands/worktree-lease-load.js +28 -0
- package/dist/cli/commands/worktree-lease-release-command.d.ts +13 -0
- package/dist/cli/commands/worktree-lease-release-command.js +105 -0
- package/dist/cli/commands/worktree-lease-renew-command.d.ts +11 -0
- package/dist/cli/commands/worktree-lease-renew-command.js +99 -0
- package/dist/cli/commands/worktree-lease-session.d.ts +22 -0
- package/dist/cli/commands/worktree-lease-session.js +29 -0
- package/dist/cli/commands/worktree-lease-spawn-command.d.ts +11 -0
- package/dist/cli/commands/worktree-lease-spawn-command.js +119 -0
- package/dist/cli/commands/worktree-lease-status-command.d.ts +11 -0
- package/dist/cli/commands/worktree-lease-status-command.js +86 -0
- package/dist/cli/commands/worktree-lease-store.d.ts +19 -0
- package/dist/cli/commands/worktree-lease-store.js +24 -0
- package/dist/cli/program.js +5 -0
- package/dist/services/distribution/mcp-install-runner.d.ts +21 -0
- package/dist/services/distribution/mcp-install-runner.js +59 -0
- package/dist/services/distribution/mcp-install.d.ts +95 -0
- package/dist/services/distribution/mcp-install.js +179 -0
- package/dist/services/distribution/plugin-manifest.d.ts +97 -0
- package/dist/services/distribution/plugin-manifest.js +126 -0
- package/dist/services/hooks/mcp-surface-gate.d.ts +19 -0
- package/dist/services/hooks/mcp-surface-gate.js +87 -0
- package/dist/services/ide/adapters/claude-code-adapter.d.ts +4 -35
- package/dist/services/ide/adapters/claude-code-adapter.js +48 -171
- package/dist/services/ide/adapters/claude-code-local-state.d.ts +28 -0
- package/dist/services/ide/adapters/claude-code-local-state.js +112 -0
- package/dist/services/ide/adapters/claude-code-model-window.d.ts +37 -0
- package/dist/services/ide/adapters/claude-code-model-window.js +113 -0
- package/dist/services/ide/ide-mcp-install-types.d.ts +78 -0
- package/dist/services/ide/ide-mcp-install-types.js +104 -0
- package/dist/services/ide/ide-types.d.ts +45 -1
- package/dist/services/ide/mcp-tool-matcher.d.ts +34 -0
- package/dist/services/ide/mcp-tool-matcher.js +84 -0
- package/dist/services/lint/npx-resolver.js +7 -0
- package/dist/services/mcp/cli-executor.d.ts +36 -0
- package/dist/services/mcp/cli-executor.js +97 -0
- package/dist/services/mcp/json-rpc.d.ts +81 -0
- package/dist/services/mcp/json-rpc.js +116 -0
- package/dist/services/mcp/memory-tool.d.ts +3 -0
- package/dist/services/mcp/memory-tool.js +62 -0
- package/dist/services/mcp/server-main.d.ts +1 -0
- package/dist/services/mcp/server-main.js +66 -0
- package/dist/services/mcp/server.d.ts +22 -0
- package/dist/services/mcp/server.js +152 -0
- package/dist/services/mcp/surface.d.ts +62 -0
- package/dist/services/mcp/surface.js +198 -0
- package/dist/services/mcp/tool-core.d.ts +42 -0
- package/dist/services/mcp/tool-core.js +137 -0
- package/dist/services/mcp/tools.d.ts +13 -0
- package/dist/services/mcp/tools.js +58 -0
- package/dist/services/readonly-surface/argv-guard.d.ts +32 -0
- package/dist/services/readonly-surface/argv-guard.js +164 -0
- package/dist/services/readonly-surface/readonly-whitelist.d.ts +147 -0
- package/dist/services/readonly-surface/readonly-whitelist.js +107 -0
- package/dist/services/readonly-surface/whitelist-generator.d.ts +64 -0
- package/dist/services/readonly-surface/whitelist-generator.js +212 -0
- package/dist/services/skills/hooks-codegate-superpowers.d.ts +1 -0
- package/dist/services/skills/hooks-codegate-superpowers.js +26 -0
- package/dist/services/skills/hooks-settings-service.js +10 -2
- package/package.json +17 -6
- package/scripts/install-skills-bundles.mjs +233 -0
- package/scripts/install-skills-fs.mjs +148 -0
- package/scripts/install-skills-ide.mjs +279 -0
- package/scripts/install-skills-output-style-default.mjs +196 -0
- package/scripts/install-skills-output-styles.mjs +121 -0
- package/scripts/install-skills-report.mjs +42 -0
- package/scripts/install-skills-user-config.mjs +198 -0
- package/scripts/install-skills.mjs +36 -1066
- package/scripts/watch-tree.mjs +140 -0
- package/scripts/watch.mjs +4 -126
- package/skills/peaks-status/SKILL.md +31 -13
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The schema introspector (spec §3 row 2): turns the hand-authored surface
|
|
3
|
+
* declaration into the generated whitelist artifact by READING the live Commander
|
|
4
|
+
* registry - never by restating it.
|
|
5
|
+
*
|
|
6
|
+
* Everything the registry can answer is taken from it:
|
|
7
|
+
* - does the command path exist,
|
|
8
|
+
* - does each `--flag` in the template exist on that command,
|
|
9
|
+
* - is that flag mandatory, and does it take a value,
|
|
10
|
+
* - which positionals does the command declare, and are they required.
|
|
11
|
+
* A template that disagrees with the registry is a BUILD FAILURE, not a warning:
|
|
12
|
+
* that is the whole point of generating the artifact instead of typing it.
|
|
13
|
+
*
|
|
14
|
+
* What the registry CANNOT answer is the constraint on a placeholder's value
|
|
15
|
+
* (`^[a-z0-9-]+$` for a request id). Those constraints come from the surface file
|
|
16
|
+
* and are marked `constraintSource: 'handwritten'`, so the one place drift can
|
|
17
|
+
* enter is visible in the artifact. When a command later declares `.choices()`,
|
|
18
|
+
* the same field flips to `introspected` with no artifact edit.
|
|
19
|
+
*
|
|
20
|
+
* DETERMINISM: no timestamps, no `Date`, no unordered iteration. Record
|
|
21
|
+
* keys follow argv order and entries follow the surface file's order, so two
|
|
22
|
+
* consecutive runs are byte-identical.
|
|
23
|
+
*/
|
|
24
|
+
import { readFileSync } from 'node:fs';
|
|
25
|
+
import { join } from 'node:path';
|
|
26
|
+
import { z } from 'zod';
|
|
27
|
+
import { repoRoot } from 'peaks-loop-shared/paths';
|
|
28
|
+
import { READONLY_WHITELIST_RELATIVE_PATH } from './readonly-whitelist.js';
|
|
29
|
+
/** The hand-authored input, relative to the package root. */
|
|
30
|
+
export const READONLY_SURFACE_RELATIVE_PATH = 'contracts/readonly-surface.json';
|
|
31
|
+
/** The schema version of the artifact this module emits. */
|
|
32
|
+
export const READONLY_WHITELIST_SCHEMA_VERSION = 1;
|
|
33
|
+
const SurfaceParamSchema = z.object({
|
|
34
|
+
type: z.enum(['slug', 'enum', 'token', 'integer', 'path']),
|
|
35
|
+
pattern: z.string().min(1).optional(),
|
|
36
|
+
values: z.array(z.string().min(1)).min(1).optional(),
|
|
37
|
+
min: z.number().int().optional(),
|
|
38
|
+
max: z.number().int().optional(),
|
|
39
|
+
default: z.number().int().optional()
|
|
40
|
+
});
|
|
41
|
+
const SurfaceEntrySchema = z.object({
|
|
42
|
+
id: z.string().min(1),
|
|
43
|
+
commandPath: z.array(z.string().min(1)).min(1),
|
|
44
|
+
argv: z.array(z.string().min(1)).min(1),
|
|
45
|
+
params: z.record(z.string(), SurfaceParamSchema)
|
|
46
|
+
});
|
|
47
|
+
export const ReadOnlySurfaceSchema = z.object({
|
|
48
|
+
schemaVersion: z.literal(1),
|
|
49
|
+
readOnlyDefinition: z.string().min(1),
|
|
50
|
+
note: z.array(z.string()).default([]),
|
|
51
|
+
tools: z.array(z.object({ tool: z.string().min(1), entries: z.array(SurfaceEntrySchema).min(1) }))
|
|
52
|
+
});
|
|
53
|
+
/** `<name>` marks a placeholder token in a surface `argv` element. */
|
|
54
|
+
const PLACEHOLDER_TOKEN = /^<([a-z][a-z0-9-]*)>$/;
|
|
55
|
+
class SurfaceDriftError extends Error {
|
|
56
|
+
constructor(message) {
|
|
57
|
+
super(message);
|
|
58
|
+
this.name = 'SurfaceDriftError';
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function resolveCommand(program, path) {
|
|
62
|
+
let current = program;
|
|
63
|
+
for (const name of path) {
|
|
64
|
+
current = current.commands.find((candidate) => candidate.name() === name);
|
|
65
|
+
if (current === undefined) {
|
|
66
|
+
throw new SurfaceDriftError(`No command registered at path: ${path.join(' ')}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return current;
|
|
70
|
+
}
|
|
71
|
+
function hasOption(command, token) {
|
|
72
|
+
return command.options.some((option) => option.long === token || option.short === token);
|
|
73
|
+
}
|
|
74
|
+
function optionTakingValue(command, token) {
|
|
75
|
+
return command.options.find((option) => {
|
|
76
|
+
if (option.long !== token && option.short !== token)
|
|
77
|
+
return false;
|
|
78
|
+
return /[<[].+[>\]]/.test(option.flags);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
/** Placeholders the registry supplies a constraint for are marked as such. */
|
|
82
|
+
function constraintSourceOf(declared, option) {
|
|
83
|
+
if (declared.type === 'enum' && option?.argChoices !== undefined)
|
|
84
|
+
return 'introspected';
|
|
85
|
+
if (option?.defaultValue !== undefined && declared.default !== undefined)
|
|
86
|
+
return 'introspected';
|
|
87
|
+
return 'handwritten';
|
|
88
|
+
}
|
|
89
|
+
/** The constraint fields the surface declares, copied without inventing absent ones. */
|
|
90
|
+
function constraintsOf(declared) {
|
|
91
|
+
return {
|
|
92
|
+
type: declared.type,
|
|
93
|
+
...(declared.pattern === undefined ? {} : { pattern: declared.pattern }),
|
|
94
|
+
...(declared.values === undefined ? {} : { values: declared.values }),
|
|
95
|
+
...(declared.min === undefined ? {} : { min: declared.min }),
|
|
96
|
+
...(declared.max === undefined ? {} : { max: declared.max }),
|
|
97
|
+
...(declared.default === undefined ? {} : { default: declared.default })
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Bind one placeholder either to the value-taking flag before it or to the next
|
|
102
|
+
* positional the command declares. Membership in the template is what makes a
|
|
103
|
+
* value required; `binding.required` carries the REGISTRY's opinion, which is a
|
|
104
|
+
* different fact (`--limit` is not mandatory, yet the template pins it).
|
|
105
|
+
*/
|
|
106
|
+
function bindPlaceholder(slot) {
|
|
107
|
+
const { entryId, name, declared, command, previousToken, cursor } = slot;
|
|
108
|
+
if (declared === undefined) {
|
|
109
|
+
throw new SurfaceDriftError(`Entry ${entryId}: placeholder <${name}> has no params spec`);
|
|
110
|
+
}
|
|
111
|
+
const boundOption = previousToken === undefined ? undefined : optionTakingValue(command, previousToken);
|
|
112
|
+
if (boundOption !== undefined && previousToken !== undefined) {
|
|
113
|
+
return {
|
|
114
|
+
...constraintsOf(declared),
|
|
115
|
+
binding: { kind: 'option', token: previousToken, required: boundOption.mandatory },
|
|
116
|
+
constraintSource: constraintSourceOf(declared, boundOption)
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
const registered = command.registeredArguments[cursor.positionalIndex];
|
|
120
|
+
if (registered === undefined) {
|
|
121
|
+
throw new SurfaceDriftError(`Entry ${entryId}: <${name}> is positional ${cursor.positionalIndex + 1} but the command declares ${command.registeredArguments.length}`);
|
|
122
|
+
}
|
|
123
|
+
cursor.positionalIndex += 1;
|
|
124
|
+
cursor.positionalNames.push(registered.name());
|
|
125
|
+
return {
|
|
126
|
+
...constraintsOf(declared),
|
|
127
|
+
binding: { kind: 'positional', token: registered.name(), required: registered.required },
|
|
128
|
+
constraintSource: 'handwritten'
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/** The non-flag words a template opens with, which must be the command path. */
|
|
132
|
+
function leadingLiteralsOf(argv) {
|
|
133
|
+
return argv.flatMap((segment) => segment.kind === 'literal' && !segment.value.startsWith('-') ? [segment.value] : []);
|
|
134
|
+
}
|
|
135
|
+
function assertTemplateDrivesCommand(surface, command, argv, cursor) {
|
|
136
|
+
const leading = leadingLiteralsOf(argv);
|
|
137
|
+
if (leading.join('\u0000') !== surface.commandPath.join('\u0000')) {
|
|
138
|
+
throw new SurfaceDriftError(`Entry ${surface.id}: argv opens with [${leading.join(', ')}] but commandPath is [${surface.commandPath.join(', ')}]`);
|
|
139
|
+
}
|
|
140
|
+
if (cursor.positionalIndex !== command.registeredArguments.length) {
|
|
141
|
+
throw new SurfaceDriftError(`Entry ${surface.id}: template fills ${cursor.positionalIndex} positional(s); ${surface.commandPath.join(' ')} declares ${command.registeredArguments.length}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function buildEntry(program, tool, surface) {
|
|
145
|
+
const command = resolveCommand(program, surface.commandPath);
|
|
146
|
+
const optionTokens = [];
|
|
147
|
+
const argv = [];
|
|
148
|
+
const params = {};
|
|
149
|
+
const cursor = { positionalIndex: 0, positionalNames: [] };
|
|
150
|
+
for (let index = 0; index < surface.argv.length; index += 1) {
|
|
151
|
+
const token = surface.argv[index];
|
|
152
|
+
if (token === undefined)
|
|
153
|
+
continue;
|
|
154
|
+
const placeholder = PLACEHOLDER_TOKEN.exec(token);
|
|
155
|
+
const name = placeholder?.[1];
|
|
156
|
+
if (placeholder !== null && name !== undefined) {
|
|
157
|
+
params[name] = bindPlaceholder({
|
|
158
|
+
entryId: surface.id,
|
|
159
|
+
name,
|
|
160
|
+
declared: surface.params[name],
|
|
161
|
+
command,
|
|
162
|
+
previousToken: index > 0 ? surface.argv[index - 1] : undefined,
|
|
163
|
+
cursor
|
|
164
|
+
});
|
|
165
|
+
argv.push({ kind: 'param', name });
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (token.startsWith('-')) {
|
|
169
|
+
if (!hasOption(command, token)) {
|
|
170
|
+
throw new SurfaceDriftError(`Entry ${surface.id}: no such option ${token} on ${surface.commandPath.join(' ')}`);
|
|
171
|
+
}
|
|
172
|
+
optionTokens.push(token);
|
|
173
|
+
}
|
|
174
|
+
argv.push({ kind: 'literal', value: token });
|
|
175
|
+
}
|
|
176
|
+
assertTemplateDrivesCommand(surface, command, argv, cursor);
|
|
177
|
+
return {
|
|
178
|
+
id: surface.id,
|
|
179
|
+
tool,
|
|
180
|
+
commandPath: [...surface.commandPath],
|
|
181
|
+
argv,
|
|
182
|
+
params,
|
|
183
|
+
introspection: { optionTokens, positionalCount: cursor.positionalNames.length }
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
/** Build the artifact object from a surface declaration and a live program. */
|
|
187
|
+
export function generateReadOnlyWhitelist(surface, program) {
|
|
188
|
+
const entries = surface.tools.flatMap((group) => group.entries.map((entry) => buildEntry(program, group.tool, entry)));
|
|
189
|
+
const ids = new Set(entries.map((entry) => entry.id));
|
|
190
|
+
if (ids.size !== entries.length) {
|
|
191
|
+
throw new SurfaceDriftError('Entry ids must be unique across the whole surface');
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
schemaVersion: READONLY_WHITELIST_SCHEMA_VERSION,
|
|
195
|
+
source: READONLY_SURFACE_RELATIVE_PATH,
|
|
196
|
+
readOnlyDefinition: surface.readOnlyDefinition,
|
|
197
|
+
entries
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
/** Byte-stable serialization: two runs over the same input MUST match. */
|
|
201
|
+
export function serializeReadOnlyWhitelist(whitelist) {
|
|
202
|
+
return `${JSON.stringify(whitelist, null, 2)}\n`;
|
|
203
|
+
}
|
|
204
|
+
/** Read + validate the hand-authored surface declaration. */
|
|
205
|
+
export function loadReadOnlySurface(root = repoRoot) {
|
|
206
|
+
const text = readFileSync(join(root, READONLY_SURFACE_RELATIVE_PATH), 'utf8');
|
|
207
|
+
return ReadOnlySurfaceSchema.parse(JSON.parse(text));
|
|
208
|
+
}
|
|
209
|
+
/** Absolute path of the generated artifact. */
|
|
210
|
+
export function readonlyWhitelistPath(root = repoRoot) {
|
|
211
|
+
return join(root, READONLY_WHITELIST_RELATIVE_PATH);
|
|
212
|
+
}
|
|
@@ -15,6 +15,7 @@ interface ResolvedHookSpec {
|
|
|
15
15
|
readonly hookEnforceSentinel: string;
|
|
16
16
|
readonly hookEnforceMatcher: string;
|
|
17
17
|
readonly hookEnforceEvent: string;
|
|
18
|
+
readonly hookEnforceMcpMatcher: string | undefined;
|
|
18
19
|
/**
|
|
19
20
|
* True when the gate-enforce entry must be materialized into the IDE's
|
|
20
21
|
* MACHINE-LOCAL settings file rather than the shared one (see
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* type contract re-exported by the caller.
|
|
9
9
|
*/
|
|
10
10
|
import { getAdapter } from '../ide/ide-registry.js';
|
|
11
|
+
import { mcpHookMatcher } from '../ide/mcp-tool-matcher.js';
|
|
11
12
|
import { HOOK_COMPACT_SETTLE_COMMAND, HOOK_COMPACT_SETTLE_EVENT, HOOK_COMPACT_SETTLE_MATCHER, HOOK_COMPACT_SETTLE_SENTINEL, HOOK_OUTER_CACHE_COMMAND, HOOK_OUTER_CACHE_EVENT, HOOK_OUTER_CACHE_SENTINEL, HOOK_POST_COMPACT_REINJECT_COMMAND, HOOK_POST_COMPACT_REINJECT_EVENT, HOOK_POST_COMPACT_REINJECT_MATCHER, HOOK_POST_COMPACT_REINJECT_SENTINEL, HOOK_WORKSPACE_INIT_COMMAND, HOOK_WORKSPACE_INIT_EVENT, HOOK_WORKSPACE_INIT_SENTINEL } from './session-start-hook-constants.js';
|
|
12
13
|
/** Sentinel substring identifying a Claude-Code gate-enforce hook entry. */
|
|
13
14
|
export const HOOK_ENFORCE_SENTINEL = 'peaks gate enforce';
|
|
@@ -87,6 +88,7 @@ export function resolveHookSpec(ide) {
|
|
|
87
88
|
hookEnforceSentinel: spec.sentinel,
|
|
88
89
|
hookEnforceMatcher: adapter.toolMatcher,
|
|
89
90
|
hookEnforceEvent: adapter.hookEvent,
|
|
91
|
+
hookEnforceMcpMatcher: mcpHookMatcher(adapter),
|
|
90
92
|
// Only Claude Code has the machine-local sibling settings file the
|
|
91
93
|
// routing depends on, and only Claude Code's hook schema accepts a
|
|
92
94
|
// `shell` key.
|
|
@@ -105,6 +107,27 @@ export const HOOK_CODE_GATE_SENTINEL = 'peaks code-gate';
|
|
|
105
107
|
export const HOOK_CODE_GATE_MATCHER = 'Edit|Write|MultiEdit';
|
|
106
108
|
export const HOOK_CODE_GATE_EVENT = 'PreToolUse';
|
|
107
109
|
export const HOOK_CODE_GATE_COMMAND = `peaks code-gate --json`;
|
|
110
|
+
/**
|
|
111
|
+
* The read-only MCP surface as a hook entry: the same command under the
|
|
112
|
+
* adapter's MCP matcher, or `undefined` for a harness that declares no MCP
|
|
113
|
+
* naming. A hook fires only for the matchers its OWN entry names, so without this
|
|
114
|
+
* entry the fail-closed branch in `mcp-surface-gate.ts` never runs on a live
|
|
115
|
+
* harness. A second entry rather than a wider matcher on the Bash one: that entry
|
|
116
|
+
* is mirrored by the workspace-init template, and widening it would put installer
|
|
117
|
+
* and template out of step.
|
|
118
|
+
*/
|
|
119
|
+
function mcpMatcherEntry(spec) {
|
|
120
|
+
if (spec.hookEnforceMcpMatcher === undefined)
|
|
121
|
+
return undefined;
|
|
122
|
+
return {
|
|
123
|
+
sentinel: spec.hookEnforceSentinel,
|
|
124
|
+
matcher: spec.hookEnforceMcpMatcher,
|
|
125
|
+
command: spec.hookEnforceCommand,
|
|
126
|
+
event: spec.hookEnforceEvent,
|
|
127
|
+
machineLocal: spec.hookEnforceMachineLocal,
|
|
128
|
+
...(spec.hookEnforceShell !== undefined ? { shell: spec.hookEnforceShell } : {})
|
|
129
|
+
};
|
|
130
|
+
}
|
|
108
131
|
export function resolveHookEntries(ide, _skipProgress = false) {
|
|
109
132
|
const spec = resolveHookSpec(ide);
|
|
110
133
|
const entries = [
|
|
@@ -117,6 +140,9 @@ export function resolveHookEntries(ide, _skipProgress = false) {
|
|
|
117
140
|
...(spec.hookEnforceShell !== undefined ? { shell: spec.hookEnforceShell } : {})
|
|
118
141
|
}
|
|
119
142
|
];
|
|
143
|
+
const mcpEntry = mcpMatcherEntry(spec);
|
|
144
|
+
if (mcpEntry !== undefined)
|
|
145
|
+
entries.push(mcpEntry);
|
|
120
146
|
// ── Why the three SessionStart entries below carry NO `shell` pin ─────────
|
|
121
147
|
//
|
|
122
148
|
// The gate-enforce entry above is shell-pinned on Windows (see
|
|
@@ -358,8 +358,16 @@ function shapeMatchesDesired(settings, entries, allPeaksSentinels) {
|
|
|
358
358
|
// source" — so a file written before matchers were explicit converges
|
|
359
359
|
// once instead of churning on every install.
|
|
360
360
|
for (const desired of entries.filter((e) => e.event === eventKey)) {
|
|
361
|
-
|
|
362
|
-
|
|
361
|
+
// The identity of a desired entry is the PAIR (sentinel, matcher), not the
|
|
362
|
+
// sentinel alone: one command may legitimately be installed under several
|
|
363
|
+
// matchers (the read-only MCP surface rides `peaks gate enforce` under its
|
|
364
|
+
// own matcher set). Matching on the sentinel first found that first entry
|
|
365
|
+
// every time, so the second one never looked installed and every install
|
|
366
|
+
// rewrote the file. A wrong matcher is still un-repaired — it fails to
|
|
367
|
+
// match and the caller rewrites.
|
|
368
|
+
const onDisk = peaksPresent.find((entry) => (entry.matcher ?? '') === desired.matcher &&
|
|
369
|
+
(entry.hooks ?? []).some((h) => String(h.command ?? '').includes(desired.sentinel)));
|
|
370
|
+
if (onDisk === undefined)
|
|
363
371
|
return false;
|
|
364
372
|
}
|
|
365
373
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "peaks-loop",
|
|
3
|
-
"version": "4.1.
|
|
3
|
+
"version": "4.1.3",
|
|
4
4
|
"description": "Loop Engineering CLI — workflow primitive / loop guards / evaluators / slice orchestration",
|
|
5
5
|
"author": "SquabbyZ",
|
|
6
6
|
"keywords": [
|
|
@@ -75,16 +75,25 @@
|
|
|
75
75
|
"scripts/copy-templates.mjs",
|
|
76
76
|
"scripts/sync-version.mjs",
|
|
77
77
|
"scripts/install-skills.mjs",
|
|
78
|
+
"scripts/install-skills-fs.mjs",
|
|
79
|
+
"scripts/install-skills-report.mjs",
|
|
80
|
+
"scripts/install-skills-user-config.mjs",
|
|
81
|
+
"scripts/install-skills-ide.mjs",
|
|
82
|
+
"scripts/install-skills-bundles.mjs",
|
|
83
|
+
"scripts/install-skills-output-styles.mjs",
|
|
84
|
+
"scripts/install-skills-output-style-default.mjs",
|
|
78
85
|
"scripts/canonical-store.mjs",
|
|
79
86
|
"scripts/canonical-store-link.mjs",
|
|
80
87
|
"scripts/canonical-store-prune.mjs",
|
|
81
88
|
"scripts/watch.mjs",
|
|
89
|
+
"scripts/watch-tree.mjs",
|
|
82
90
|
"skills/**",
|
|
83
91
|
"!skills/**/test-prompts.json",
|
|
84
92
|
"!skills/**/.DS_Store",
|
|
85
93
|
"output-styles/**",
|
|
86
94
|
"agents/**",
|
|
87
95
|
"schemas/*.json",
|
|
96
|
+
"contracts/readonly-argv-whitelist.json",
|
|
88
97
|
".claude-plugin/**",
|
|
89
98
|
"config/eslint/.peaks-rules.cjs",
|
|
90
99
|
"config/eslint/tsconfig.lint.json",
|
|
@@ -120,10 +129,10 @@
|
|
|
120
129
|
"picomatch": "4.0.4",
|
|
121
130
|
"yaml": "^2.9.0",
|
|
122
131
|
"zod": "^4.4.3",
|
|
123
|
-
"peaks-loop-internal-runtime": "0.0.
|
|
124
|
-
"peaks-loop-
|
|
125
|
-
"peaks-loop-
|
|
126
|
-
"peaks-loop-shared": "0.0.
|
|
132
|
+
"peaks-loop-internal-runtime": "0.0.43",
|
|
133
|
+
"peaks-loop-mut": "0.1.57",
|
|
134
|
+
"peaks-loop-shared-channel": "0.0.60",
|
|
135
|
+
"peaks-loop-shared": "0.0.92"
|
|
127
136
|
},
|
|
128
137
|
"devDependencies": {
|
|
129
138
|
"@changesets/cli": "2.31.1",
|
|
@@ -148,7 +157,9 @@
|
|
|
148
157
|
"vitest": "^4.1.10"
|
|
149
158
|
},
|
|
150
159
|
"scripts": {
|
|
151
|
-
"build": "node ./scripts/sync-version.mjs && node ./scripts/clean-dist.mjs && pnpm -r --filter \"./packages/*\" run build && node ./scripts/write-package-dist-stamps.mjs && tsc -p tsconfig.build.json && node ./scripts/copy-templates.mjs && node ./scripts/check-build-integrity.mjs && node ./scripts/write-dist-stamp.mjs",
|
|
160
|
+
"build": "node ./scripts/sync-version.mjs && node ./scripts/clean-dist.mjs && pnpm -r --filter \"./packages/*\" run build && node ./scripts/write-package-dist-stamps.mjs && node node_modules/tsx/dist/cli.mjs scripts/generate-readonly-whitelist.ts && node node_modules/tsx/dist/cli.mjs scripts/generate-plugin-manifest.ts && tsc -p tsconfig.build.json && node ./scripts/copy-templates.mjs && node ./scripts/check-build-integrity.mjs && node ./scripts/write-dist-stamp.mjs",
|
|
161
|
+
"readonly:whitelist": "node node_modules/tsx/dist/cli.mjs scripts/generate-readonly-whitelist.ts",
|
|
162
|
+
"plugin:manifest": "node node_modules/tsx/dist/cli.mjs scripts/generate-plugin-manifest.ts",
|
|
152
163
|
"prepublish": "node ./scripts/sync-version.mjs",
|
|
153
164
|
"postinstall": "node ./scripts/install-skills.mjs",
|
|
154
165
|
"predev": "node ./scripts/sync-version.mjs",
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
// scripts/install-skills-bundles.mjs
|
|
2
|
+
//
|
|
3
|
+
// The two installers that read a directory of bundled assets out of the package and
|
|
4
|
+
// reconcile each entry into an IDE directory: the DIRECTORY-shaped family (skills) and
|
|
5
|
+
// the FILE-shaped one (agents). Each owns its dispatch precedence and its repair rule.
|
|
6
|
+
//
|
|
7
|
+
// Split out of `scripts/install-skills.mjs` (rid-043). Every line below was moved
|
|
8
|
+
// VERBATIM from that file; only this header, the import block, and the `export`
|
|
9
|
+
// keyword on names a sibling imports are new.
|
|
10
|
+
|
|
11
|
+
import { existsSync, lstatSync, mkdirSync, readdirSync } from 'node:fs';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
|
|
14
|
+
import { reconcileCanonicalEntry } from './canonical-store.mjs';
|
|
15
|
+
import { reconcileCanonicalFileEntry } from './canonical-store-link.mjs';
|
|
16
|
+
import { pruneBundledEntries } from './canonical-store-prune.mjs';
|
|
17
|
+
import { createInstallResult } from './install-skills-report.mjs';
|
|
18
|
+
import { createInstallRootValidator, resolvePackageRoot } from './install-skills-fs.mjs';
|
|
19
|
+
import { resolveBundleTargetRoot } from './install-skills-ide.mjs';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* THE INSTALL CANDIDATE LIST — every skill the package ships, as
|
|
23
|
+
* `{ skillName, sourcePath }`.
|
|
24
|
+
*
|
|
25
|
+
* After the v2.13.0 bee-demote (commit de0872b), the role skills (peaks-prd,
|
|
26
|
+
* peaks-rd, peaks-qa, peaks-ui, peaks-sc, peaks-txt) moved under
|
|
27
|
+
* `skills/bee/<role>/` while user-facing helpers stayed at `skills/<name>/`. So the
|
|
28
|
+
* list is built by walking top-level entries (user-facing helpers) AND
|
|
29
|
+
* `skills/bee/<role>` (demoted role skills). Each candidate is installed under its
|
|
30
|
+
* basename, so the postinstall links `~/.claude/skills/peaks-rd` to
|
|
31
|
+
* `skills/bee/peaks-rd` rather than to `skills/bee`.
|
|
32
|
+
*
|
|
33
|
+
* De-dupe rule: a top-level helper with the same name WINS, which preserves the
|
|
34
|
+
* existing install contract for any helper sharing a name with a demoted role skill
|
|
35
|
+
* (legacy overlap).
|
|
36
|
+
*
|
|
37
|
+
* @param {string} skillsRoot
|
|
38
|
+
* @returns {Array<{ skillName: string, sourcePath: string }>}
|
|
39
|
+
*/
|
|
40
|
+
function collectSkillCandidates(skillsRoot) {
|
|
41
|
+
const beeRoot = join(skillsRoot, 'bee');
|
|
42
|
+
/** @type {Array<{ skillName: string, sourcePath: string }>} */
|
|
43
|
+
const candidates = [];
|
|
44
|
+
for (const skillName of readdirSync(skillsRoot)) {
|
|
45
|
+
if (skillName === 'bee') continue;
|
|
46
|
+
const sourcePath = join(skillsRoot, skillName);
|
|
47
|
+
if (!isSkillSource(sourcePath)) continue;
|
|
48
|
+
candidates.push({ skillName, sourcePath });
|
|
49
|
+
}
|
|
50
|
+
if (existsSync(beeRoot) && isDirectoryEntry(beeRoot)) {
|
|
51
|
+
for (const skillName of readdirSync(beeRoot)) {
|
|
52
|
+
const sourcePath = join(beeRoot, skillName);
|
|
53
|
+
if (!isSkillSource(sourcePath)) continue;
|
|
54
|
+
if (candidates.some((c) => c.skillName === skillName)) continue;
|
|
55
|
+
candidates.push({ skillName, sourcePath });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return candidates;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** A real directory entry — `lstat`, so a symlink to one is NOT one. */
|
|
62
|
+
function isDirectoryEntry(entryPath) {
|
|
63
|
+
return lstatSync(entryPath).isDirectory();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** A directory entry that is a skill: a real directory holding a `SKILL.md`. */
|
|
67
|
+
function isSkillSource(sourcePath) {
|
|
68
|
+
return isDirectoryEntry(sourcePath) && existsSync(join(sourcePath, 'SKILL.md'));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function installBundledSkills(options = {}) {
|
|
72
|
+
const packageRoot = resolvePackageRoot(options);
|
|
73
|
+
const skillsRoot = join(packageRoot, 'skills');
|
|
74
|
+
|
|
75
|
+
if (process.env.PEAKS_SKIP_SKILL_INSTALL === '1' || !existsSync(skillsRoot)) {
|
|
76
|
+
return createInstallResult();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Slice #011: IDE-aware dispatch — the precedence itself now lives in ONE place
|
|
80
|
+
// (`resolveBundleTargetRoot` in `install-skills-ide.mjs`), because all three
|
|
81
|
+
// families stated it and only this one also warns.
|
|
82
|
+
const targetRoot = resolveBundleTargetRoot(options, {
|
|
83
|
+
profileField: 'skillsDir',
|
|
84
|
+
envVar: 'PEAKS_CLAUDE_SKILLS_DIR',
|
|
85
|
+
fallback: '.claude/skills',
|
|
86
|
+
warn: true
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const installed = [];
|
|
90
|
+
const skipped = [];
|
|
91
|
+
mkdirSync(targetRoot, { recursive: true });
|
|
92
|
+
const validateSkillsRoot = createInstallRootValidator(targetRoot, 'Peaks skills');
|
|
93
|
+
|
|
94
|
+
const candidates = collectSkillCandidates(skillsRoot);
|
|
95
|
+
|
|
96
|
+
for (const { skillName, sourcePath } of candidates) {
|
|
97
|
+
const targetPath = join(targetRoot, skillName);
|
|
98
|
+
|
|
99
|
+
// Slice 2 (`agents-canonical-store`) — the entry points at the CANONICAL
|
|
100
|
+
// STORE (`~/.peaks/skills/<name>`), not at `<packageRoot>/skills/<name>`,
|
|
101
|
+
// because `<packageRoot>` changes on every `npm i -g peaks-loop@latest` and
|
|
102
|
+
// the links must bind to a PATH peaks-loop owns, not to a VERSION. Repair is
|
|
103
|
+
// the only behaviour: an entry peaks-loop owns (sidecar matches, or the link
|
|
104
|
+
// dangles) is relinked onto the store; only a real directory the user authored
|
|
105
|
+
// is left alone and reported `skipped`. `options.reconcileJunctions` is
|
|
106
|
+
// therefore READ NOWHERE here — still accepted (`sync-service.ts` forwards it)
|
|
107
|
+
// so passing it changes nothing, and removing it is a CLI-surface change this
|
|
108
|
+
// slice does not own.
|
|
109
|
+
validateSkillsRoot();
|
|
110
|
+
const reconciled = reconcileCanonicalEntry({
|
|
111
|
+
kind: 'skills',
|
|
112
|
+
name: skillName,
|
|
113
|
+
sourcePath,
|
|
114
|
+
linkPath: targetPath
|
|
115
|
+
});
|
|
116
|
+
validateSkillsRoot();
|
|
117
|
+
|
|
118
|
+
if (reconciled.linkAction === 'skipped') {
|
|
119
|
+
skipped.push(skillName);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
installed.push(skillName);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Slice 3 — PRUNE. The loop above only ever ADDS. A skill removed from the
|
|
126
|
+
// package left its canonical copy, every IDE link and every sidecar on the
|
|
127
|
+
// user's machine forever, because nothing walked the DESTINATIONS. This does,
|
|
128
|
+
// and deletes only what `isManagedEntry` proves is ours.
|
|
129
|
+
const { pruned } = pruneBundledEntries({
|
|
130
|
+
kind: 'skills',
|
|
131
|
+
keepNames: candidates.map((candidate) => candidate.skillName),
|
|
132
|
+
linkDirs: [targetRoot]
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
return { installed, skipped, pruned };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Slice 7/7 — bundled agents (Claude Code sub-agent prompts).
|
|
140
|
+
*
|
|
141
|
+
* Writes into the IDE's sub-agent loader directory (`~/.claude/agents/` for Claude
|
|
142
|
+
* Code, and whatever `agentsDir` the detected profile declares). The bytes live in
|
|
143
|
+
* the canonical store (`~/.peaks/agents/<name>`) and the IDE entry is a link to it.
|
|
144
|
+
*
|
|
145
|
+
* Ownership policy — ONE predicate, in `canonical-store-link.mjs::isOwnedFileEntry`,
|
|
146
|
+
* and it is deliberately not a string comparison:
|
|
147
|
+
* - entry resolves to the canonical copy → leave it (idempotent run)
|
|
148
|
+
* - entry is a link we wrote (sidecar agrees, dangles) → relink onto the store
|
|
149
|
+
* - entry is a real file with OUR sidecar beside it → re-point it at the store
|
|
150
|
+
* - entry is a real file with a LEGACY sidecar whose
|
|
151
|
+
* recorded package path no longer resolves → same: repair it
|
|
152
|
+
* - entry is a real file we never wrote (no sidecar) → skip (user-authored)
|
|
153
|
+
* - entry is a real file whose sidecar names a path
|
|
154
|
+
* that STILL resolves → skip (a second live install)
|
|
155
|
+
*
|
|
156
|
+
* The two "skip" arms above are the ones the old code got wrong in opposite
|
|
157
|
+
* directions: it skipped whenever the package path merely DIFFERED, which froze
|
|
158
|
+
* every entry on the first upgrade, silently and permanently. See
|
|
159
|
+
* `isOwnedFileEntry` for why a vanished package path is proof of an earlier install.
|
|
160
|
+
*/
|
|
161
|
+
export function installBundledAgents(options = {}) {
|
|
162
|
+
const packageRoot = resolvePackageRoot(options);
|
|
163
|
+
const agentsRoot = join(packageRoot, 'agents');
|
|
164
|
+
|
|
165
|
+
// Per-IDE env-var override (claude-code only today): PEAKS_CLAUDE_AGENTS_DIR.
|
|
166
|
+
// Universal escape hatch: PEAKS_SKIP_AGENT_INSTALL=1 (parallel to
|
|
167
|
+
// PEAKS_SKIP_SKILL_INSTALL).
|
|
168
|
+
if (
|
|
169
|
+
process.env.PEAKS_SKIP_SKILL_INSTALL === '1' ||
|
|
170
|
+
process.env.PEAKS_SKIP_AGENT_INSTALL === '1' ||
|
|
171
|
+
!existsSync(agentsRoot)
|
|
172
|
+
) {
|
|
173
|
+
return createInstallResult();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Slice #011: IDE-aware dispatch — same precedence as the other two families,
|
|
177
|
+
// resolved by the same helper (`resolveBundleTargetRoot`).
|
|
178
|
+
const targetRoot = resolveBundleTargetRoot(options, {
|
|
179
|
+
profileField: 'agentsDir',
|
|
180
|
+
envVar: 'PEAKS_CLAUDE_AGENTS_DIR',
|
|
181
|
+
fallback: '.claude/agents'
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const installed = [];
|
|
185
|
+
const skipped = [];
|
|
186
|
+
const fallbacks = [];
|
|
187
|
+
mkdirSync(targetRoot, { recursive: true });
|
|
188
|
+
const validateAgentsRoot = createInstallRootValidator(targetRoot, 'Peaks agents');
|
|
189
|
+
|
|
190
|
+
// Slice 4 (`agents-canonical-store`) — the canonical store owns the bytes and the
|
|
191
|
+
// IDE entry points at it. Everything this loop used to decide about ownership is
|
|
192
|
+
// now `reconcileCanonicalFileEntry`'s, and the reason is the defect the brief
|
|
193
|
+
// names as this slice's core: the old test was
|
|
194
|
+
// `resolve(marker.sourcePath) === resolve(sourcePath)`, a STRING comparison against
|
|
195
|
+
// the package path of the installing version — a path with a node version inside it
|
|
196
|
+
// (`…\nvm\v24.21.0\node_modules\peaks-loop\agents\…`). It went false the first time
|
|
197
|
+
// the package moved and stayed false, so the agent silently never updated again.
|
|
198
|
+
const bundledAgentNames = [];
|
|
199
|
+
for (const agentFileName of readdirSync(agentsRoot)) {
|
|
200
|
+
const sourcePath = join(agentsRoot, agentFileName);
|
|
201
|
+
if (!lstatSync(sourcePath).isFile() || !agentFileName.endsWith('.md')) continue;
|
|
202
|
+
bundledAgentNames.push(agentFileName);
|
|
203
|
+
|
|
204
|
+
validateAgentsRoot();
|
|
205
|
+
const reconciled = reconcileCanonicalFileEntry({
|
|
206
|
+
kind: 'agents',
|
|
207
|
+
name: agentFileName,
|
|
208
|
+
sourcePath,
|
|
209
|
+
linkPath: join(targetRoot, agentFileName),
|
|
210
|
+
createFileLink: options.createFileLink
|
|
211
|
+
});
|
|
212
|
+
validateAgentsRoot();
|
|
213
|
+
|
|
214
|
+
if (reconciled.linkAction === 'skipped') {
|
|
215
|
+
skipped.push(agentFileName);
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
installed.push(agentFileName);
|
|
219
|
+
if (reconciled.fallback !== null) {
|
|
220
|
+
fallbacks.push({ name: agentFileName, ...reconciled.fallback });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// The delete half: an agent the package stops shipping used to leave its canonical
|
|
225
|
+
// copy, its IDE entry and both sidecars on the machine forever.
|
|
226
|
+
const { pruned } = pruneBundledEntries({
|
|
227
|
+
kind: 'agents',
|
|
228
|
+
keepNames: bundledAgentNames,
|
|
229
|
+
linkDirs: [targetRoot]
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
return { installed, skipped, pruned, fallbacks };
|
|
233
|
+
}
|