@specship/specship-win32-arm64 0.24.0 → 0.28.0
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/bin/specship.cmd +31 -1
- package/lib/dist/activation/cheatsheet.d.ts +17 -1
- package/lib/dist/activation/cheatsheet.d.ts.map +1 -1
- package/lib/dist/activation/cheatsheet.js +47 -3
- package/lib/dist/activation/cheatsheet.js.map +1 -1
- package/lib/dist/activation/hook-output.d.ts +80 -0
- package/lib/dist/activation/hook-output.d.ts.map +1 -0
- package/lib/dist/activation/hook-output.js +103 -0
- package/lib/dist/activation/hook-output.js.map +1 -0
- package/lib/dist/activation/search-intercept.d.ts +60 -29
- package/lib/dist/activation/search-intercept.d.ts.map +1 -1
- package/lib/dist/activation/search-intercept.js +159 -55
- package/lib/dist/activation/search-intercept.js.map +1 -1
- package/lib/dist/activation/steering.d.ts +11 -13
- package/lib/dist/activation/steering.d.ts.map +1 -1
- package/lib/dist/activation/steering.js +25 -10
- package/lib/dist/activation/steering.js.map +1 -1
- package/lib/dist/behaviour/behaviour-surface.d.ts +47 -0
- package/lib/dist/behaviour/behaviour-surface.d.ts.map +1 -1
- package/lib/dist/behaviour/behaviour-surface.js +108 -2
- package/lib/dist/behaviour/behaviour-surface.js.map +1 -1
- package/lib/dist/bin/node-version-check.d.ts +22 -7
- package/lib/dist/bin/node-version-check.d.ts.map +1 -1
- package/lib/dist/bin/node-version-check.js +39 -9
- package/lib/dist/bin/node-version-check.js.map +1 -1
- package/lib/dist/bin/specship.js +989 -116
- package/lib/dist/bin/specship.js.map +1 -1
- package/lib/dist/commands/specship/learn.md +27 -3
- package/lib/dist/commands/specship/spec.md +85 -15
- package/lib/dist/db/spec-queries.d.ts +73 -3
- package/lib/dist/db/spec-queries.d.ts.map +1 -1
- package/lib/dist/db/spec-queries.js +139 -5
- package/lib/dist/db/spec-queries.js.map +1 -1
- package/lib/dist/db/sqlite-adapter.d.ts +7 -5
- package/lib/dist/db/sqlite-adapter.d.ts.map +1 -1
- package/lib/dist/db/sqlite-adapter.js +11 -9
- package/lib/dist/db/sqlite-adapter.js.map +1 -1
- package/lib/dist/enforce/changed-files.d.ts +39 -0
- package/lib/dist/enforce/changed-files.d.ts.map +1 -0
- package/lib/dist/enforce/changed-files.js +114 -0
- package/lib/dist/enforce/changed-files.js.map +1 -0
- package/lib/dist/enforce/enforce.d.ts +53 -2
- package/lib/dist/enforce/enforce.d.ts.map +1 -1
- package/lib/dist/enforce/enforce.js +95 -8
- package/lib/dist/enforce/enforce.js.map +1 -1
- package/lib/dist/enforce/mutation.d.ts +46 -0
- package/lib/dist/enforce/mutation.d.ts.map +1 -0
- package/lib/dist/enforce/mutation.js +104 -0
- package/lib/dist/enforce/mutation.js.map +1 -0
- package/lib/dist/enforce/suppressions.d.ts +33 -0
- package/lib/dist/enforce/suppressions.d.ts.map +1 -0
- package/lib/dist/enforce/suppressions.js +159 -0
- package/lib/dist/enforce/suppressions.js.map +1 -0
- package/lib/dist/extraction/grammars.d.ts +10 -0
- package/lib/dist/extraction/grammars.d.ts.map +1 -1
- package/lib/dist/extraction/grammars.js +32 -3
- package/lib/dist/extraction/grammars.js.map +1 -1
- package/lib/dist/extraction/index.d.ts +7 -0
- package/lib/dist/extraction/index.d.ts.map +1 -1
- package/lib/dist/extraction/index.js.map +1 -1
- package/lib/dist/extraction/specs/markdown-spec-extractor.d.ts +7 -0
- package/lib/dist/extraction/specs/markdown-spec-extractor.d.ts.map +1 -1
- package/lib/dist/extraction/specs/markdown-spec-extractor.js +97 -10
- package/lib/dist/extraction/specs/markdown-spec-extractor.js.map +1 -1
- package/lib/dist/extraction/wasm/tree-sitter-pascal.wasm.gz +0 -0
- package/lib/dist/extraction/wasm/tree-sitter-scala.wasm.gz +0 -0
- package/lib/dist/graph/spec-coverage.d.ts +142 -0
- package/lib/dist/graph/spec-coverage.d.ts.map +1 -0
- package/lib/dist/graph/spec-coverage.js +276 -0
- package/lib/dist/graph/spec-coverage.js.map +1 -0
- package/lib/dist/health/smoke-check.js +2 -2
- package/lib/dist/health/smoke-check.js.map +1 -1
- package/lib/dist/index.d.ts +75 -7
- package/lib/dist/index.d.ts.map +1 -1
- package/lib/dist/index.js +191 -23
- package/lib/dist/index.js.map +1 -1
- package/lib/dist/installer/gemini-commands.d.ts +107 -0
- package/lib/dist/installer/gemini-commands.d.ts.map +1 -0
- package/lib/dist/installer/gemini-commands.js +264 -0
- package/lib/dist/installer/gemini-commands.js.map +1 -0
- package/lib/dist/installer/index.d.ts +55 -8
- package/lib/dist/installer/index.d.ts.map +1 -1
- package/lib/dist/installer/index.js +72 -53
- package/lib/dist/installer/index.js.map +1 -1
- package/lib/dist/installer/init-offer.d.ts +14 -9
- package/lib/dist/installer/init-offer.d.ts.map +1 -1
- package/lib/dist/installer/init-offer.js +13 -9
- package/lib/dist/installer/init-offer.js.map +1 -1
- package/lib/dist/installer/install-defaults.d.ts +81 -0
- package/lib/dist/installer/install-defaults.d.ts.map +1 -0
- package/lib/dist/installer/install-defaults.js +95 -0
- package/lib/dist/installer/install-defaults.js.map +1 -0
- package/lib/dist/installer/instructions-template.d.ts +22 -0
- package/lib/dist/installer/instructions-template.d.ts.map +1 -1
- package/lib/dist/installer/instructions-template.js +37 -1
- package/lib/dist/installer/instructions-template.js.map +1 -1
- package/lib/dist/installer/targets/claude.d.ts +18 -0
- package/lib/dist/installer/targets/claude.d.ts.map +1 -1
- package/lib/dist/installer/targets/claude.js +70 -23
- package/lib/dist/installer/targets/claude.js.map +1 -1
- package/lib/dist/installer/targets/gemini.d.ts +85 -12
- package/lib/dist/installer/targets/gemini.d.ts.map +1 -1
- package/lib/dist/installer/targets/gemini.js +527 -30
- package/lib/dist/installer/targets/gemini.js.map +1 -1
- package/lib/dist/installer/targets/shared.d.ts +13 -0
- package/lib/dist/installer/targets/shared.d.ts.map +1 -1
- package/lib/dist/installer/targets/shared.js +22 -4
- package/lib/dist/installer/targets/shared.js.map +1 -1
- package/lib/dist/installer/targets/types.d.ts +12 -0
- package/lib/dist/installer/targets/types.d.ts.map +1 -1
- package/lib/dist/jira/auto-publish.d.ts +71 -4
- package/lib/dist/jira/auto-publish.d.ts.map +1 -1
- package/lib/dist/jira/auto-publish.js +186 -17
- package/lib/dist/jira/auto-publish.js.map +1 -1
- package/lib/dist/jira/reconcile.d.ts +24 -1
- package/lib/dist/jira/reconcile.d.ts.map +1 -1
- package/lib/dist/jira/reconcile.js +46 -2
- package/lib/dist/jira/reconcile.js.map +1 -1
- package/lib/dist/jira/repo-config.d.ts +9 -0
- package/lib/dist/jira/repo-config.d.ts.map +1 -1
- package/lib/dist/jira/repo-config.js +11 -0
- package/lib/dist/jira/repo-config.js.map +1 -1
- package/lib/dist/jira/types.d.ts +7 -0
- package/lib/dist/jira/types.d.ts.map +1 -1
- package/lib/dist/jira/types.js.map +1 -1
- package/lib/dist/knowledge/enforce.d.ts +75 -0
- package/lib/dist/knowledge/enforce.d.ts.map +1 -0
- package/lib/dist/knowledge/enforce.js +250 -0
- package/lib/dist/knowledge/enforce.js.map +1 -0
- package/lib/dist/knowledge/facts.d.ts +33 -0
- package/lib/dist/knowledge/facts.d.ts.map +1 -0
- package/lib/dist/knowledge/facts.js +117 -0
- package/lib/dist/knowledge/facts.js.map +1 -0
- package/lib/dist/knowledge/proposals.d.ts +88 -0
- package/lib/dist/knowledge/proposals.d.ts.map +1 -0
- package/lib/dist/knowledge/proposals.js +360 -0
- package/lib/dist/knowledge/proposals.js.map +1 -0
- package/lib/dist/knowledge/stale.d.ts +64 -0
- package/lib/dist/knowledge/stale.d.ts.map +1 -0
- package/lib/dist/knowledge/stale.js +174 -0
- package/lib/dist/knowledge/stale.js.map +1 -0
- package/lib/dist/mcp/fitness-tool.d.ts +5 -6
- package/lib/dist/mcp/fitness-tool.d.ts.map +1 -1
- package/lib/dist/mcp/fitness-tool.js +0 -16
- package/lib/dist/mcp/fitness-tool.js.map +1 -1
- package/lib/dist/mcp/gemini-session.d.ts +50 -0
- package/lib/dist/mcp/gemini-session.d.ts.map +1 -0
- package/lib/dist/mcp/gemini-session.js +159 -0
- package/lib/dist/mcp/gemini-session.js.map +1 -0
- package/lib/dist/mcp/health-tool.d.ts +26 -0
- package/lib/dist/mcp/health-tool.d.ts.map +1 -0
- package/lib/dist/mcp/health-tool.js +50 -0
- package/lib/dist/mcp/health-tool.js.map +1 -0
- package/lib/dist/mcp/jira-tools.d.ts.map +1 -1
- package/lib/dist/mcp/jira-tools.js +25 -0
- package/lib/dist/mcp/jira-tools.js.map +1 -1
- package/lib/dist/mcp/maintainability-tool.d.ts +5 -7
- package/lib/dist/mcp/maintainability-tool.d.ts.map +1 -1
- package/lib/dist/mcp/maintainability-tool.js +0 -16
- package/lib/dist/mcp/maintainability-tool.js.map +1 -1
- package/lib/dist/mcp/model-context.d.ts +31 -7
- package/lib/dist/mcp/model-context.d.ts.map +1 -1
- package/lib/dist/mcp/model-context.js +70 -21
- package/lib/dist/mcp/model-context.js.map +1 -1
- package/lib/dist/mcp/proxy.d.ts.map +1 -1
- package/lib/dist/mcp/proxy.js +5 -2
- package/lib/dist/mcp/proxy.js.map +1 -1
- package/lib/dist/mcp/server-instructions.d.ts +16 -15
- package/lib/dist/mcp/server-instructions.d.ts.map +1 -1
- package/lib/dist/mcp/server-instructions.js +72 -5
- package/lib/dist/mcp/server-instructions.js.map +1 -1
- package/lib/dist/mcp/session.js +1 -1
- package/lib/dist/mcp/session.js.map +1 -1
- package/lib/dist/mcp/spec-tools.d.ts +9 -1
- package/lib/dist/mcp/spec-tools.d.ts.map +1 -1
- package/lib/dist/mcp/spec-tools.js +282 -49
- package/lib/dist/mcp/spec-tools.js.map +1 -1
- package/lib/dist/mcp/tools.d.ts +49 -9
- package/lib/dist/mcp/tools.d.ts.map +1 -1
- package/lib/dist/mcp/tools.js +210 -58
- package/lib/dist/mcp/tools.js.map +1 -1
- package/lib/dist/pulse/pulse.d.ts +93 -0
- package/lib/dist/pulse/pulse.d.ts.map +1 -0
- package/lib/dist/pulse/pulse.js +401 -0
- package/lib/dist/pulse/pulse.js.map +1 -0
- package/lib/dist/reflect/apply.d.ts.map +1 -1
- package/lib/dist/reflect/apply.js +38 -3
- package/lib/dist/reflect/apply.js.map +1 -1
- package/lib/dist/reflect/auto-sweep.d.ts +26 -0
- package/lib/dist/reflect/auto-sweep.d.ts.map +1 -0
- package/lib/dist/reflect/auto-sweep.js +90 -0
- package/lib/dist/reflect/auto-sweep.js.map +1 -0
- package/lib/dist/reflect/cli.d.ts +62 -0
- package/lib/dist/reflect/cli.d.ts.map +1 -0
- package/lib/dist/reflect/cli.js +163 -0
- package/lib/dist/reflect/cli.js.map +1 -0
- package/lib/dist/reflect/miner.d.ts.map +1 -1
- package/lib/dist/reflect/miner.js +119 -0
- package/lib/dist/reflect/miner.js.map +1 -1
- package/lib/dist/reflect/sweep.d.ts +1 -0
- package/lib/dist/reflect/sweep.d.ts.map +1 -1
- package/lib/dist/reflect/sweep.js +62 -13
- package/lib/dist/reflect/sweep.js.map +1 -1
- package/lib/dist/reflect/targets.d.ts +2 -0
- package/lib/dist/reflect/targets.d.ts.map +1 -1
- package/lib/dist/reflect/targets.js +33 -0
- package/lib/dist/reflect/targets.js.map +1 -1
- package/lib/dist/reflect/types.d.ts +19 -2
- package/lib/dist/reflect/types.d.ts.map +1 -1
- package/lib/dist/resolution/spec-link-resolver.d.ts +44 -2
- package/lib/dist/resolution/spec-link-resolver.d.ts.map +1 -1
- package/lib/dist/resolution/spec-link-resolver.js +132 -0
- package/lib/dist/resolution/spec-link-resolver.js.map +1 -1
- package/lib/dist/resolution/test-title-links.d.ts +62 -0
- package/lib/dist/resolution/test-title-links.d.ts.map +1 -0
- package/lib/dist/resolution/test-title-links.js +97 -0
- package/lib/dist/resolution/test-title-links.js.map +1 -0
- package/lib/dist/search/query-parser.d.ts.map +1 -1
- package/lib/dist/search/query-parser.js +6 -1
- package/lib/dist/search/query-parser.js.map +1 -1
- package/lib/dist/server/project-registry.js +1 -1
- package/lib/dist/server/routes/claude.js +24 -43
- package/lib/dist/server/routes/graph.js +53 -7
- package/lib/dist/server/routes/knowledge.js +118 -0
- package/lib/dist/server/routes/mcp.js +4 -3
- package/lib/dist/server/routes/projects.js +4 -4
- package/lib/dist/server/routes/spec.js +49 -7
- package/lib/dist/server/server.js +31 -17
- package/lib/dist/skills/spec-author/SKILL.md +164 -0
- package/lib/dist/skills/spec-author/references/format.md +210 -0
- package/lib/dist/skills/spec-author/references/gap-questions.md +152 -0
- package/lib/dist/skills/spec-author/references/quality-rubric.md +125 -0
- package/lib/dist/skills/spec-author/references/review-checklist.md +175 -0
- package/lib/dist/spec-lint/lint.d.ts +82 -0
- package/lib/dist/spec-lint/lint.d.ts.map +1 -0
- package/lib/dist/spec-lint/lint.js +491 -0
- package/lib/dist/spec-lint/lint.js.map +1 -0
- package/lib/dist/spec-quality/criterion-ids.d.ts +13 -0
- package/lib/dist/spec-quality/criterion-ids.d.ts.map +1 -0
- package/lib/dist/spec-quality/criterion-ids.js +38 -0
- package/lib/dist/spec-quality/criterion-ids.js.map +1 -0
- package/lib/dist/spec-quality/evidence-kind.d.ts +54 -0
- package/lib/dist/spec-quality/evidence-kind.d.ts.map +1 -0
- package/lib/dist/spec-quality/evidence-kind.js +183 -0
- package/lib/dist/spec-quality/evidence-kind.js.map +1 -0
- package/lib/dist/spec-quality/link-persist.d.ts +65 -0
- package/lib/dist/spec-quality/link-persist.d.ts.map +1 -0
- package/lib/dist/spec-quality/link-persist.js +249 -0
- package/lib/dist/spec-quality/link-persist.js.map +1 -0
- package/lib/dist/spec-quality/plan-scope.d.ts +38 -0
- package/lib/dist/spec-quality/plan-scope.d.ts.map +1 -0
- package/lib/dist/spec-quality/plan-scope.js +129 -0
- package/lib/dist/spec-quality/plan-scope.js.map +1 -0
- package/lib/dist/spec-quality/postcheck.d.ts +85 -0
- package/lib/dist/spec-quality/postcheck.d.ts.map +1 -0
- package/lib/dist/spec-quality/postcheck.js +390 -0
- package/lib/dist/spec-quality/postcheck.js.map +1 -0
- package/lib/dist/spec-quality/readiness.d.ts +72 -0
- package/lib/dist/spec-quality/readiness.d.ts.map +1 -0
- package/lib/dist/spec-quality/readiness.js +226 -0
- package/lib/dist/spec-quality/readiness.js.map +1 -0
- package/lib/dist/statusline/index.js.map +1 -1
- package/lib/dist/statusline/render.d.ts +1 -1
- package/lib/dist/statusline/render.d.ts.map +1 -1
- package/lib/dist/statusline/render.js +3 -1
- package/lib/dist/statusline/render.js.map +1 -1
- package/lib/dist/ui/assets/index-BzuUWUAE.js +50 -0
- package/lib/dist/ui/index.html +1 -1
- package/lib/dist/verify/report-ingest.d.ts +88 -0
- package/lib/dist/verify/report-ingest.d.ts.map +1 -0
- package/lib/dist/verify/report-ingest.js +269 -0
- package/lib/dist/verify/report-ingest.js.map +1 -0
- package/lib/dist/workflows/agents/claude.d.ts +16 -0
- package/lib/dist/workflows/agents/claude.d.ts.map +1 -0
- package/lib/dist/workflows/agents/claude.js +82 -0
- package/lib/dist/workflows/agents/claude.js.map +1 -0
- package/lib/dist/workflows/agents/gemini.d.ts +21 -0
- package/lib/dist/workflows/agents/gemini.d.ts.map +1 -0
- package/lib/dist/workflows/agents/gemini.js +124 -0
- package/lib/dist/workflows/agents/gemini.js.map +1 -0
- package/lib/dist/workflows/agents/index.d.ts +28 -0
- package/lib/dist/workflows/agents/index.d.ts.map +1 -0
- package/lib/dist/workflows/agents/index.js +104 -0
- package/lib/dist/workflows/agents/index.js.map +1 -0
- package/lib/dist/workflows/agents/types.d.ts +55 -0
- package/lib/dist/workflows/agents/types.d.ts.map +1 -0
- package/lib/dist/workflows/agents/types.js +46 -0
- package/lib/dist/workflows/agents/types.js.map +1 -0
- package/lib/dist/workflows/batch.d.ts +63 -0
- package/lib/dist/workflows/batch.d.ts.map +1 -0
- package/lib/dist/workflows/batch.js +151 -0
- package/lib/dist/workflows/batch.js.map +1 -0
- package/lib/dist/workflows/defaults/claude-design-implement.yaml +3 -4
- package/lib/dist/workflows/defaults/spec-author.yaml +11 -9
- package/lib/dist/workflows/defaults/spec-fix.yaml +103 -12
- package/lib/dist/workflows/defaults/spec-implement-mixed.yaml +21 -193
- package/lib/dist/workflows/defaults/spec-implement.yaml +701 -41
- package/lib/dist/workflows/defaults/spec-verify.yaml +60 -9
- package/lib/dist/workflows/discovery.d.ts +13 -0
- package/lib/dist/workflows/discovery.d.ts.map +1 -1
- package/lib/dist/workflows/discovery.js +79 -17
- package/lib/dist/workflows/discovery.js.map +1 -1
- package/lib/dist/workflows/executor.d.ts +18 -0
- package/lib/dist/workflows/executor.d.ts.map +1 -1
- package/lib/dist/workflows/executor.js +130 -33
- package/lib/dist/workflows/executor.js.map +1 -1
- package/lib/dist/workflows/gc.d.ts +50 -0
- package/lib/dist/workflows/gc.d.ts.map +1 -0
- package/lib/dist/workflows/gc.js +120 -0
- package/lib/dist/workflows/gc.js.map +1 -0
- package/lib/dist/workflows/merge-queue.d.ts +64 -0
- package/lib/dist/workflows/merge-queue.d.ts.map +1 -0
- package/lib/dist/workflows/merge-queue.js +162 -0
- package/lib/dist/workflows/merge-queue.js.map +1 -0
- package/lib/dist/workflows/runners/approval.d.ts +1 -1
- package/lib/dist/workflows/runners/approval.d.ts.map +1 -1
- package/lib/dist/workflows/runners/approval.js +11 -2
- package/lib/dist/workflows/runners/approval.js.map +1 -1
- package/lib/dist/workflows/runners/bash.d.ts +15 -0
- package/lib/dist/workflows/runners/bash.d.ts.map +1 -1
- package/lib/dist/workflows/runners/bash.js +50 -1
- package/lib/dist/workflows/runners/bash.js.map +1 -1
- package/lib/dist/workflows/runners/prompt.d.ts +16 -41
- package/lib/dist/workflows/runners/prompt.d.ts.map +1 -1
- package/lib/dist/workflows/runners/prompt.js +55 -120
- package/lib/dist/workflows/runners/prompt.js.map +1 -1
- package/lib/dist/workflows/runners/types.d.ts +12 -0
- package/lib/dist/workflows/runners/types.d.ts.map +1 -1
- package/lib/dist/workflows/schemas/workflow.d.ts +17 -0
- package/lib/dist/workflows/schemas/workflow.d.ts.map +1 -1
- package/lib/dist/workflows/schemas/workflow.js +27 -0
- package/lib/dist/workflows/schemas/workflow.js.map +1 -1
- package/lib/node_modules/.package-lock.json +1 -1
- package/lib/package.json +8 -4
- package/package.json +5 -3
- package/lib/dist/extraction/wasm/tree-sitter-pascal.wasm +0 -0
- package/lib/dist/extraction/wasm/tree-sitter-scala.wasm +0 -0
- package/lib/dist/server/ingest/impact-query.js +0 -343
- package/lib/dist/server/routes/events.js +0 -152
- package/lib/dist/ui/assets/index-CjjaftDw.js +0 -50
- package/node.exe +0 -4
package/lib/dist/bin/specship.js
CHANGED
|
@@ -268,9 +268,10 @@ if (nodeMajor >= 25) {
|
|
|
268
268
|
// Enforce the supported Node floor. `engines` in package.json only *warns* on
|
|
269
269
|
// install (unless engine-strict), so hard-block here to actually keep users off
|
|
270
270
|
// unsupported versions. Mirrors the 25+ block above. See package.json `engines`.
|
|
271
|
-
|
|
271
|
+
const nodeFloor = (0, node_version_check_1.nodeFloorDecision)(nodeVersion);
|
|
272
|
+
if (nodeFloor !== 'ok') {
|
|
272
273
|
process.stderr.write((0, node_version_check_1.buildNodeTooOldBanner)(nodeVersion) + '\n');
|
|
273
|
-
if (
|
|
274
|
+
if (nodeFloor === 'block') {
|
|
274
275
|
process.exit(1);
|
|
275
276
|
}
|
|
276
277
|
// Override active — banner shown for visibility, continuing.
|
|
@@ -862,6 +863,45 @@ function main() {
|
|
|
862
863
|
catch {
|
|
863
864
|
/* surfacing is best-effort */
|
|
864
865
|
}
|
|
866
|
+
// Stale external facts (REQ-KNOW-002): one line when any dependency
|
|
867
|
+
// moved past the version a fact was verified against. Local
|
|
868
|
+
// node_modules only on this path — no `npm root -g` spawn at session start.
|
|
869
|
+
try {
|
|
870
|
+
const { findStaleExternalFacts } = require('../knowledge/stale');
|
|
871
|
+
const stale = findStaleExternalFacts(projectPath, { globalRoot: () => null }).stale;
|
|
872
|
+
if (stale.length > 0) {
|
|
873
|
+
console.log(`⚠ ${stale.length} external fact(s) stale (dependency changed since verified) — review with: specship knowledge stale`);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
catch {
|
|
877
|
+
/* surfacing is best-effort */
|
|
878
|
+
}
|
|
879
|
+
// Pipeline pulse (REQ-PIPETRUST-007): one line per failing item from
|
|
880
|
+
// the CACHED pulse only — no network on this path; silent when healthy.
|
|
881
|
+
try {
|
|
882
|
+
const { readCachedPulse, formatPulseLines } = require('../pulse/pulse');
|
|
883
|
+
const cached = readCachedPulse(projectPath);
|
|
884
|
+
if (cached) {
|
|
885
|
+
for (const line of formatPulseLines(cached.items))
|
|
886
|
+
console.log(line);
|
|
887
|
+
}
|
|
888
|
+
// Keep the cache warm without blocking the session: when it is missing
|
|
889
|
+
// or older than six hours, refresh it in a detached child (bounded by
|
|
890
|
+
// pulse's own time budget). This session prints what was cached; the
|
|
891
|
+
// next one sees the refreshed result.
|
|
892
|
+
const SIX_HOURS = 6 * 60 * 60 * 1000;
|
|
893
|
+
if ((!cached || Date.now() - cached.checkedAt > SIX_HOURS) && !process.env.SPECSHIP_NO_PULSE) {
|
|
894
|
+
const { spawn } = require('child_process');
|
|
895
|
+
const child = spawn(process.execPath, [process.argv[1], 'pulse', projectPath, '--refresh', '--json'], {
|
|
896
|
+
detached: true,
|
|
897
|
+
stdio: 'ignore',
|
|
898
|
+
});
|
|
899
|
+
child.unref();
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
catch {
|
|
903
|
+
/* surfacing is best-effort */
|
|
904
|
+
}
|
|
865
905
|
};
|
|
866
906
|
try {
|
|
867
907
|
if (!(0, directory_1.isInitialized)(projectPath)) {
|
|
@@ -893,12 +933,32 @@ function main() {
|
|
|
893
933
|
/* summary is best-effort */
|
|
894
934
|
}
|
|
895
935
|
};
|
|
936
|
+
// Guarded auto-learn sweep (REQ-ALEARN-004): cooldown-bounded, opt-out
|
|
937
|
+
// via reflect.autoSweep=false, and NEVER allowed to fail the sync (A3).
|
|
938
|
+
const runAutoSweep = async (instance) => {
|
|
939
|
+
try {
|
|
940
|
+
const { loadReflectConfig, shouldAutoSweep, recordAutoSweep } = await Promise.resolve().then(() => __importStar(require('../reflect/auto-sweep')));
|
|
941
|
+
const cfg = loadReflectConfig(projectPath);
|
|
942
|
+
if (!shouldAutoSweep(projectPath, Date.now(), cfg))
|
|
943
|
+
return;
|
|
944
|
+
recordAutoSweep(projectPath, Date.now());
|
|
945
|
+
const { notify } = instance.reflectSweep();
|
|
946
|
+
const { learnedNotice } = await Promise.resolve().then(() => __importStar(require('../reflect/cli')));
|
|
947
|
+
for (const p of notify) {
|
|
948
|
+
console.log(learnedNotice(p.title));
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
catch {
|
|
952
|
+
/* a sweep failure never blocks a sync (REQ-ALEARN-004.A3) */
|
|
953
|
+
}
|
|
954
|
+
};
|
|
896
955
|
if (options.quiet) {
|
|
897
956
|
const result = await cg.sync();
|
|
898
957
|
pushDriftNotices(result);
|
|
899
958
|
await pushJiraDriftComments(cg, result);
|
|
900
959
|
await pushJiraAutoPublish(result);
|
|
901
960
|
pushDriftSummary(cg);
|
|
961
|
+
await runAutoSweep(cg);
|
|
902
962
|
cg.destroy();
|
|
903
963
|
return;
|
|
904
964
|
}
|
|
@@ -925,10 +985,23 @@ function main() {
|
|
|
925
985
|
details.push(`Removed: ${result.filesRemoved}`);
|
|
926
986
|
clack.log.info(`${details.join(', ')} ${(0, glyphs_1.getGlyphs)().dash} ${formatNumber(result.nodesUpdated)} nodes in ${formatDuration(result.durationMs)}`);
|
|
927
987
|
}
|
|
988
|
+
// Spec parse problems from the sync's spec pass (REQ-REVINT-002.A2) —
|
|
989
|
+
// silent before, so a bare-path link bullet produced no edge and no word.
|
|
990
|
+
const specErrors = result.specErrors ?? 0;
|
|
991
|
+
const specWarnings = result.specWarnings ?? 0;
|
|
992
|
+
if (specErrors > 0 || specWarnings > 0) {
|
|
993
|
+
const parts = [];
|
|
994
|
+
if (specErrors > 0)
|
|
995
|
+
parts.push(`${specErrors} error${specErrors === 1 ? '' : 's'}`);
|
|
996
|
+
if (specWarnings > 0)
|
|
997
|
+
parts.push(`${specWarnings} warning${specWarnings === 1 ? '' : 's'}`);
|
|
998
|
+
clack.log.warn(`Spec parse: ${parts.join(', ')}`);
|
|
999
|
+
}
|
|
928
1000
|
pushDriftNotices(result);
|
|
929
1001
|
await pushJiraDriftComments(cg, result);
|
|
930
1002
|
await pushJiraAutoPublish(result);
|
|
931
1003
|
pushDriftSummary(cg);
|
|
1004
|
+
await runAutoSweep(cg);
|
|
932
1005
|
clack.outro('Done');
|
|
933
1006
|
cg.destroy();
|
|
934
1007
|
}
|
|
@@ -1346,15 +1419,30 @@ function main() {
|
|
|
1346
1419
|
* Run a reflection pass over the ingested Claude Code transcripts and print the
|
|
1347
1420
|
* self-improvement proposals it surfaces (REQ-REFLECT-006.A1). Headless — no
|
|
1348
1421
|
* dashboard required. With no usable transcript history, prints an empty state.
|
|
1422
|
+
*
|
|
1423
|
+
* Review verbs (REQ-AUDFIX-002): `specship reflect apply <hash> [--yes]`,
|
|
1424
|
+
* `specship reflect undo <hash>`, `specship reflect dismiss <hash>`. The first
|
|
1425
|
+
* positional is a verb when it is exactly apply/undo/dismiss, otherwise the
|
|
1426
|
+
* project path (so `specship reflect` and `specship reflect <path>` are
|
|
1427
|
+
* unchanged); verb mode takes its project from --path (default: cwd).
|
|
1349
1428
|
*/
|
|
1350
1429
|
program
|
|
1351
|
-
.command('reflect [path]')
|
|
1352
|
-
.description('Mine ingested transcripts for self-improvement proposals
|
|
1430
|
+
.command('reflect [path-or-verb] [hash]')
|
|
1431
|
+
.description('Mine ingested transcripts for self-improvement proposals; review them with apply <hash> [--yes] / undo <hash> / dismiss <hash>')
|
|
1353
1432
|
.option('-j, --json', 'Output as JSON')
|
|
1433
|
+
.option('-p, --path <path>', 'Project path (for apply/undo/dismiss; default: cwd)')
|
|
1434
|
+
.option('-y, --yes', 'With apply: write the change (without it, apply only previews)')
|
|
1354
1435
|
.option('--capture', 'Capture a routine as a skill proposal: --title required, content on stdin (LEARN-DOC)')
|
|
1355
1436
|
.option('--title <title>', 'Title for --capture')
|
|
1356
|
-
.
|
|
1357
|
-
|
|
1437
|
+
.option('--lane <lane>', 'Capture lane: skill (default), style (project convention → .specship/conventions.md), or domain (draft fact → specs/domain/) (ALEARN-DOC)')
|
|
1438
|
+
.action(async (firstArg, hashArg, options) => {
|
|
1439
|
+
const { isReflectVerb, runReflectVerb, REFLECT_VERBS, REFLECT_REVIEW_HINT, DISPLAY_HASH_LEN } = await Promise.resolve().then(() => __importStar(require('../reflect/cli')));
|
|
1440
|
+
const verb = isReflectVerb(firstArg) ? firstArg : undefined;
|
|
1441
|
+
if (!verb && hashArg !== undefined) {
|
|
1442
|
+
error(`Unknown reflect action: ${firstArg}. Valid: ${REFLECT_VERBS.join(', ')} (or a project path with no further arguments)`);
|
|
1443
|
+
process.exit(1);
|
|
1444
|
+
}
|
|
1445
|
+
const projectPath = resolveProjectPath(verb ? options.path : (firstArg ?? options.path));
|
|
1358
1446
|
try {
|
|
1359
1447
|
if (!(0, directory_1.isInitialized)(projectPath)) {
|
|
1360
1448
|
error(`SpecShip not initialized in ${projectPath}`);
|
|
@@ -1362,6 +1450,17 @@ function main() {
|
|
|
1362
1450
|
}
|
|
1363
1451
|
const { default: SpecShip } = await loadSpecShip();
|
|
1364
1452
|
const cg = await SpecShip.open(projectPath);
|
|
1453
|
+
// Review verbs (REQ-AUDFIX-002): apply / undo / dismiss by hash prefix.
|
|
1454
|
+
if (verb) {
|
|
1455
|
+
const code = runReflectVerb(cg, verb, hashArg, { yes: options.yes, json: options.json }, {
|
|
1456
|
+
log: (m) => console.log(m),
|
|
1457
|
+
error: (m) => error(m),
|
|
1458
|
+
});
|
|
1459
|
+
cg.destroy();
|
|
1460
|
+
if (code !== 0)
|
|
1461
|
+
process.exit(code);
|
|
1462
|
+
return;
|
|
1463
|
+
}
|
|
1365
1464
|
// Explicit capture door (LEARN-DOC, REQ-LEARN-002): crystallize a
|
|
1366
1465
|
// distilled routine into a human-gated skill proposal. Content on
|
|
1367
1466
|
// stdin so multi-line routines survive shell quoting.
|
|
@@ -1378,13 +1477,18 @@ function main() {
|
|
|
1378
1477
|
error('--capture expects the routine content on stdin (pipe or heredoc)');
|
|
1379
1478
|
process.exit(1);
|
|
1380
1479
|
}
|
|
1381
|
-
const
|
|
1480
|
+
const lane = options.lane;
|
|
1481
|
+
if (lane && !['skill', 'style', 'domain'].includes(lane)) {
|
|
1482
|
+
error(`Unknown --lane: ${lane}. Valid: skill, style, domain`);
|
|
1483
|
+
process.exit(1);
|
|
1484
|
+
}
|
|
1485
|
+
const p = cg.reflectCapture({ title: options.title, content, lane });
|
|
1382
1486
|
if (options.json) {
|
|
1383
1487
|
console.log(JSON.stringify(p, null, 2));
|
|
1384
1488
|
}
|
|
1385
1489
|
else {
|
|
1386
1490
|
info(`Captured as proposal ${p.contentHash.slice(0, 12)} (${p.state}): ${p.title}`);
|
|
1387
|
-
info(
|
|
1491
|
+
info(`Review the open proposals with \`specship reflect\`; apply this one with \`specship reflect apply ${p.contentHash.slice(0, DISPLAY_HASH_LEN)}\` — nothing lands until you apply it.`);
|
|
1388
1492
|
}
|
|
1389
1493
|
cg.destroy();
|
|
1390
1494
|
return;
|
|
@@ -1395,7 +1499,9 @@ function main() {
|
|
|
1395
1499
|
cg.destroy();
|
|
1396
1500
|
return;
|
|
1397
1501
|
}
|
|
1398
|
-
|
|
1502
|
+
// Stored open proposals (incl. explicit captures) list even when this
|
|
1503
|
+
// pass mined nothing new — they are what apply/dismiss act on.
|
|
1504
|
+
if (result.open.length === 0) {
|
|
1399
1505
|
info('No proposals yet — not enough signal in the ingested transcripts.');
|
|
1400
1506
|
info('Tip: run the dashboard with `specship desktop --ingest` to build transcript history.');
|
|
1401
1507
|
cg.destroy();
|
|
@@ -1414,13 +1520,14 @@ function main() {
|
|
|
1414
1520
|
console.log(chalk.bold(`\nReflection proposals (${result.open.length}):\n`));
|
|
1415
1521
|
for (const p of result.open) {
|
|
1416
1522
|
const sev = (sevColor[p.severity] ?? chalk.white)(p.severity.toUpperCase().padEnd(5));
|
|
1417
|
-
console.log(`${sev} ${chalk.bold(p.title)}`);
|
|
1523
|
+
console.log(`${sev} ${chalk.bold(p.title)} ${chalk.cyan(p.contentHash.slice(0, DISPLAY_HASH_LEN))}`);
|
|
1418
1524
|
console.log(chalk.dim(` ${typeLabel[p.type] ?? p.type} → ${p.targetPath}`));
|
|
1419
1525
|
console.log(chalk.dim(` ${p.evidence.detail}`));
|
|
1420
1526
|
console.log(chalk.dim(` ${p.body}`));
|
|
1421
1527
|
console.log();
|
|
1422
1528
|
}
|
|
1423
|
-
info('
|
|
1529
|
+
info('These are proposals only — nothing is written until you apply one.');
|
|
1530
|
+
info(REFLECT_REVIEW_HINT);
|
|
1424
1531
|
cg.destroy();
|
|
1425
1532
|
}
|
|
1426
1533
|
catch (err) {
|
|
@@ -1472,7 +1579,7 @@ function main() {
|
|
|
1472
1579
|
else {
|
|
1473
1580
|
info(`Captured lesson as proposal ${p.contentHash.slice(0, 12)} (${p.state}): ${p.title}`);
|
|
1474
1581
|
info(`Target: ${p.targetKind === 'claude_md' ? 'project CLAUDE.md' : 'portable ~/.claude memory note'} — nothing written yet.`);
|
|
1475
|
-
info('Review
|
|
1582
|
+
info('Review with `specship reflect`; manage applied memory rules with `specship memory list`.');
|
|
1476
1583
|
}
|
|
1477
1584
|
cg.destroy();
|
|
1478
1585
|
}
|
|
@@ -1720,6 +1827,322 @@ function main() {
|
|
|
1720
1827
|
process.exit(1);
|
|
1721
1828
|
}
|
|
1722
1829
|
});
|
|
1830
|
+
/**
|
|
1831
|
+
* specship coverage [spec_id]
|
|
1832
|
+
*
|
|
1833
|
+
* The spec coverage rollup (REQ-VSTATE-006): per requirement and per acceptance
|
|
1834
|
+
* criterion, how much of the spec has TEST evidence — `implements` links never
|
|
1835
|
+
* count. Deterministic and cheap, which is what lets the implement workflow's
|
|
1836
|
+
* human gate quote a real number instead of an LLM's count (REQ-VSTATE-005.A2).
|
|
1837
|
+
*/
|
|
1838
|
+
program
|
|
1839
|
+
.command('coverage [spec]')
|
|
1840
|
+
.description('Show spec test-coverage: per requirement and criterion, tests-link state and verdict')
|
|
1841
|
+
.option('-p, --path <path>', 'Project path')
|
|
1842
|
+
.option('-j, --json', 'Output as JSON')
|
|
1843
|
+
.option('--line', 'Print only the one-line "N of M acceptance criteria …" summary')
|
|
1844
|
+
.option('--require-kinds', 'Exit non-zero when any criterion in scope lacks its required evidence kind')
|
|
1845
|
+
.action(async (specArg, options) => {
|
|
1846
|
+
const projectPath = resolveProjectPath(options.path);
|
|
1847
|
+
try {
|
|
1848
|
+
if (!(0, directory_1.isInitialized)(projectPath)) {
|
|
1849
|
+
error(`SpecShip not initialized in ${projectPath}`);
|
|
1850
|
+
process.exit(1);
|
|
1851
|
+
}
|
|
1852
|
+
const { default: SpecShip, formatCoverageLine, requireKindsExitCode, criteriaMissingKind } = await loadSpecShip();
|
|
1853
|
+
const cg = await SpecShip.open(projectPath);
|
|
1854
|
+
const report = cg.getSpecCoverage(specArg);
|
|
1855
|
+
// REQ-PIPETRUST-003.A2: --require-kinds gates on evidence kind; the
|
|
1856
|
+
// verdicts themselves are unchanged.
|
|
1857
|
+
const kindsExit = requireKindsExitCode(report, !!options.requireKinds);
|
|
1858
|
+
if (options.json) {
|
|
1859
|
+
console.log(JSON.stringify(report, null, 2));
|
|
1860
|
+
cg.destroy();
|
|
1861
|
+
if (kindsExit !== 0)
|
|
1862
|
+
process.exit(kindsExit);
|
|
1863
|
+
return;
|
|
1864
|
+
}
|
|
1865
|
+
if (options.line) {
|
|
1866
|
+
console.log(formatCoverageLine(report));
|
|
1867
|
+
cg.destroy();
|
|
1868
|
+
if (kindsExit !== 0)
|
|
1869
|
+
process.exit(kindsExit);
|
|
1870
|
+
return;
|
|
1871
|
+
}
|
|
1872
|
+
if (report.requirements.length === 0) {
|
|
1873
|
+
info(specArg
|
|
1874
|
+
? `No requirements under "${specArg}" — nothing to report.`
|
|
1875
|
+
: 'No requirements indexed yet.');
|
|
1876
|
+
cg.destroy();
|
|
1877
|
+
return;
|
|
1878
|
+
}
|
|
1879
|
+
const mark = {
|
|
1880
|
+
verified: chalk.green('✔'),
|
|
1881
|
+
tested: chalk.yellow('~'),
|
|
1882
|
+
untested: chalk.dim('·'),
|
|
1883
|
+
broken: chalk.red('✗'),
|
|
1884
|
+
};
|
|
1885
|
+
// Evidence kind per criterion (REQ-PIPETRUST-003.A1): required vs present,
|
|
1886
|
+
// flagged `needs <kind>` when the tests don't include the required kind.
|
|
1887
|
+
const kindNote = (c) => {
|
|
1888
|
+
const present = c.presentKinds.length > 0 ? c.presentKinds.join(',') : 'none';
|
|
1889
|
+
const base = `kind: requires ${c.requiredKind}, has ${present}`;
|
|
1890
|
+
return c.kindSatisfied ? chalk.dim(base) : `${chalk.dim(base)} ${chalk.yellow(`needs ${c.requiredKind}`)}`;
|
|
1891
|
+
};
|
|
1892
|
+
for (const req of report.requirements) {
|
|
1893
|
+
console.log(`${mark[req.verdict]} ${chalk.bold(req.specId)} ${chalk.dim(req.verdict)} — ${req.title}` +
|
|
1894
|
+
(req.criteria.length === 0 ? ` ${kindNote(req)}` : ''));
|
|
1895
|
+
for (const c of req.criteria) {
|
|
1896
|
+
console.log(` ${mark[c.verdict]} ${c.specId} ${chalk.dim(`${c.verdict} · implements:${c.implementsLinks.count} tests:${c.testsLinks.count}`)} · ${kindNote(c)}`);
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
console.log();
|
|
1900
|
+
console.log(formatCoverageLine(report));
|
|
1901
|
+
const missing = criteriaMissingKind(report);
|
|
1902
|
+
if (missing.length > 0) {
|
|
1903
|
+
console.log(`${missing.length} criteria lack their required evidence kind.`);
|
|
1904
|
+
}
|
|
1905
|
+
cg.destroy();
|
|
1906
|
+
if (kindsExit !== 0)
|
|
1907
|
+
process.exit(kindsExit);
|
|
1908
|
+
}
|
|
1909
|
+
catch (err) {
|
|
1910
|
+
error(`coverage failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1911
|
+
process.exit(1);
|
|
1912
|
+
}
|
|
1913
|
+
});
|
|
1914
|
+
/**
|
|
1915
|
+
* specship lint [paths...]
|
|
1916
|
+
*
|
|
1917
|
+
* Spec-authoring lint (AUTHG-DOC). A thin registration: all rules, rendering
|
|
1918
|
+
* and the exit code live in `src/spec-lint/lint.ts` (`runLintCli` returns the
|
|
1919
|
+
* text and the code rather than printing or exiting, which is what makes it
|
|
1920
|
+
* testable). Needs no index — it reads the files directly, so it runs in a
|
|
1921
|
+
* fresh checkout and in CI before anything has been indexed.
|
|
1922
|
+
*/
|
|
1923
|
+
program
|
|
1924
|
+
.command('lint [paths...]')
|
|
1925
|
+
.description('Lint spec files for authoring problems (exits non-zero on an error finding)')
|
|
1926
|
+
.option('-p, --path <path>', 'Project path (paths are resolved against it; defaults to specs/)')
|
|
1927
|
+
.option('-j, --json', 'Output as JSON')
|
|
1928
|
+
.action(async (paths, options) => {
|
|
1929
|
+
const projectRoot = resolveProjectPath(options.path);
|
|
1930
|
+
try {
|
|
1931
|
+
const { runLintCli } = await Promise.resolve().then(() => __importStar(require('../spec-lint/lint')));
|
|
1932
|
+
const { output, exitCode } = runLintCli([...(paths ?? []), ...(options.json ? ['--json'] : [])], { projectRoot });
|
|
1933
|
+
console.log(output);
|
|
1934
|
+
if (exitCode !== 0)
|
|
1935
|
+
process.exit(exitCode);
|
|
1936
|
+
}
|
|
1937
|
+
catch (err) {
|
|
1938
|
+
error(`lint failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1939
|
+
process.exit(1);
|
|
1940
|
+
}
|
|
1941
|
+
});
|
|
1942
|
+
/**
|
|
1943
|
+
* specship knowledge <action> — the knowledge layer (KNOWLEDGE-DOC).
|
|
1944
|
+
* stale external facts whose dependency version changed (REQ-KNOW-002/006)
|
|
1945
|
+
* check enforced rules → violations (REQ-KNOW-003)
|
|
1946
|
+
* proposals pending proposals drafted by the implement workflow (REQ-KNOW-004)
|
|
1947
|
+
* apply <id> move a confirmed proposal into specs/domain/ (human-only)
|
|
1948
|
+
* reject <id> delete a proposal
|
|
1949
|
+
* File-based: needs no index, so it runs in CI and inside run worktrees.
|
|
1950
|
+
*/
|
|
1951
|
+
program
|
|
1952
|
+
.command('knowledge <action> [id]')
|
|
1953
|
+
.description('Knowledge layer: stale | check | proposals | apply <id> | reject <id>')
|
|
1954
|
+
.option('-p, --path <path>', 'Project path (defaults to cwd)')
|
|
1955
|
+
.option('-j, --json', 'Output as JSON')
|
|
1956
|
+
.action(async (action, id, options) => {
|
|
1957
|
+
const projectRoot = options.path ? path.resolve(options.path) : process.cwd();
|
|
1958
|
+
const out = (value, textOut) => console.log(options.json ? JSON.stringify(value, null, 2) : textOut);
|
|
1959
|
+
try {
|
|
1960
|
+
if (action === 'stale') {
|
|
1961
|
+
const { findStaleExternalFacts, formatStaleReport, exitCodeForStale } = await Promise.resolve().then(() => __importStar(require('../knowledge/stale')));
|
|
1962
|
+
const result = findStaleExternalFacts(projectRoot);
|
|
1963
|
+
out(result, formatStaleReport(result));
|
|
1964
|
+
process.exit(exitCodeForStale(result));
|
|
1965
|
+
}
|
|
1966
|
+
else if (action === 'check') {
|
|
1967
|
+
const { checkEnforcedRules, formatEnforceReport, exitCodeForEnforce } = await Promise.resolve().then(() => __importStar(require('../knowledge/enforce')));
|
|
1968
|
+
const result = checkEnforcedRules(projectRoot);
|
|
1969
|
+
out(result, formatEnforceReport(result));
|
|
1970
|
+
process.exit(exitCodeForEnforce(result));
|
|
1971
|
+
}
|
|
1972
|
+
else if (action === 'proposals') {
|
|
1973
|
+
const { listProposals, formatProposalList } = await Promise.resolve().then(() => __importStar(require('../knowledge/proposals')));
|
|
1974
|
+
const list = listProposals(projectRoot);
|
|
1975
|
+
out(list, formatProposalList(list));
|
|
1976
|
+
}
|
|
1977
|
+
else if (action === 'apply' || action === 'reject') {
|
|
1978
|
+
if (!id) {
|
|
1979
|
+
error(`knowledge ${action} needs a proposal id — see \`specship knowledge proposals\``);
|
|
1980
|
+
process.exit(1);
|
|
1981
|
+
}
|
|
1982
|
+
const { applyProposal, rejectProposal, ProposalRefusedError } = await Promise.resolve().then(() => __importStar(require('../knowledge/proposals')));
|
|
1983
|
+
if (action === 'reject') {
|
|
1984
|
+
rejectProposal(projectRoot, id);
|
|
1985
|
+
out({ rejected: id }, `Rejected proposal ${id} — nothing was written to specs/domain/.`);
|
|
1986
|
+
}
|
|
1987
|
+
else {
|
|
1988
|
+
try {
|
|
1989
|
+
const applied = applyProposal(projectRoot, id);
|
|
1990
|
+
out(applied, `Applied ${id} as ${applied.id} → ${applied.path}. Run \`specship sync\` to index it.`);
|
|
1991
|
+
}
|
|
1992
|
+
catch (err) {
|
|
1993
|
+
if (err instanceof ProposalRefusedError) {
|
|
1994
|
+
error(`Refused to apply ${id} — fix the proposal first:`);
|
|
1995
|
+
for (const f of err.findings)
|
|
1996
|
+
console.log(` ${f.severity} ${f.code}: ${f.message}`);
|
|
1997
|
+
process.exit(1);
|
|
1998
|
+
}
|
|
1999
|
+
throw err;
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
else {
|
|
2004
|
+
error(`unknown knowledge action "${action}" — expected stale | check | proposals | apply | reject`);
|
|
2005
|
+
process.exit(1);
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
catch (err) {
|
|
2009
|
+
error(`knowledge ${action} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2010
|
+
process.exit(1);
|
|
2011
|
+
}
|
|
2012
|
+
});
|
|
2013
|
+
/**
|
|
2014
|
+
* specship plan-scope <SPEC_ID> --plan-file <f> — small-plan auto-approval
|
|
2015
|
+
* check (THROUGHPUT-DOC, REQ-THRU-003). Always exits 0; the AUTO_APPROVE
|
|
2016
|
+
* marker is what the plan gate reads.
|
|
2017
|
+
*/
|
|
2018
|
+
program
|
|
2019
|
+
.command('plan-scope <specId>')
|
|
2020
|
+
.description('Decide whether a plan may skip the plan gate (opt-in; prints AUTO_APPROVE=yes|no)')
|
|
2021
|
+
.requiredOption('--plan-file <file>', 'File holding the plan text')
|
|
2022
|
+
.option('-p, --path <path>', 'Project path (defaults to cwd)')
|
|
2023
|
+
.action(async (specId, options) => {
|
|
2024
|
+
const projectRoot = options.path ? path.resolve(options.path) : process.cwd();
|
|
2025
|
+
try {
|
|
2026
|
+
const { assessPlanScopeFromFile, formatPlanScope } = await Promise.resolve().then(() => __importStar(require('../spec-quality/plan-scope')));
|
|
2027
|
+
const planText = fs.readFileSync(path.resolve(options.planFile), 'utf8');
|
|
2028
|
+
console.log(formatPlanScope(assessPlanScopeFromFile(projectRoot, specId, planText)));
|
|
2029
|
+
}
|
|
2030
|
+
catch (err) {
|
|
2031
|
+
console.log(`AUTO_APPROVE=no\n- plan-scope failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2032
|
+
}
|
|
2033
|
+
});
|
|
2034
|
+
/**
|
|
2035
|
+
* specship ready <SPEC_ID> — readiness gate (PIPETRUST-DOC, REQ-PIPETRUST-004).
|
|
2036
|
+
* File-based (no index needed), so it runs inside a workflow worktree. Exits
|
|
2037
|
+
* non-zero when the requirement is not ready; prints the evidence plan.
|
|
2038
|
+
*/
|
|
2039
|
+
program
|
|
2040
|
+
.command('ready <specId>')
|
|
2041
|
+
.description('Check a requirement is ready to implement and print its evidence plan (exits non-zero when not ready)')
|
|
2042
|
+
.option('-p, --path <path>', 'Project path (defaults to cwd)')
|
|
2043
|
+
.option('-j, --json', 'Output as JSON')
|
|
2044
|
+
.action(async (specId, options) => {
|
|
2045
|
+
const projectRoot = options.path ? path.resolve(options.path) : process.cwd();
|
|
2046
|
+
try {
|
|
2047
|
+
const { assessReadinessFromFile, formatEvidencePlan } = await Promise.resolve().then(() => __importStar(require('../spec-quality/readiness')));
|
|
2048
|
+
const result = assessReadinessFromFile(projectRoot, specId);
|
|
2049
|
+
console.log(options.json ? JSON.stringify(result, null, 2) : formatEvidencePlan(result));
|
|
2050
|
+
if (!result.ready)
|
|
2051
|
+
process.exit(1);
|
|
2052
|
+
}
|
|
2053
|
+
catch (err) {
|
|
2054
|
+
error(`ready failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2055
|
+
process.exit(1);
|
|
2056
|
+
}
|
|
2057
|
+
});
|
|
2058
|
+
/**
|
|
2059
|
+
* specship postcheck <SPEC_ID> [--fix] — mechanical post-implement checks
|
|
2060
|
+
* (PIPETRUST-DOC, REQ-PIPETRUST-005). File-based, worktree-safe.
|
|
2061
|
+
*/
|
|
2062
|
+
program
|
|
2063
|
+
.command('postcheck <specId>')
|
|
2064
|
+
.description('Mechanically check an implemented requirement: verifies coverage, CHANGELOG, configured checks (exits non-zero on failure)')
|
|
2065
|
+
.option('-p, --path <path>', 'Project path (defaults to cwd)')
|
|
2066
|
+
.option('--fix', 'Run each configured check\'s fix command first')
|
|
2067
|
+
.option('--base <ref>', 'Base ref for the CHANGELOG check (defaults to the recorded run base, then the merge-base with the default branch)')
|
|
2068
|
+
.option('-j, --json', 'Output as JSON')
|
|
2069
|
+
.action(async (specId, options) => {
|
|
2070
|
+
const projectRoot = options.path ? path.resolve(options.path) : process.cwd();
|
|
2071
|
+
try {
|
|
2072
|
+
const { runPostcheck, formatPostcheck } = await Promise.resolve().then(() => __importStar(require('../spec-quality/postcheck')));
|
|
2073
|
+
const result = runPostcheck({ projectRoot, requirementId: specId, fix: options.fix, base: options.base });
|
|
2074
|
+
console.log(options.json ? JSON.stringify(result, null, 2) : formatPostcheck(result));
|
|
2075
|
+
if (!result.ok)
|
|
2076
|
+
process.exit(1);
|
|
2077
|
+
}
|
|
2078
|
+
catch (err) {
|
|
2079
|
+
error(`postcheck failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2080
|
+
process.exit(1);
|
|
2081
|
+
}
|
|
2082
|
+
});
|
|
2083
|
+
/**
|
|
2084
|
+
* specship verify --report <file>
|
|
2085
|
+
*
|
|
2086
|
+
* Derive verification from a REAL test run (REQ-VSTATE-004): read a vitest JSON
|
|
2087
|
+
* report, match reported cases to `tests` links by file + test title, and
|
|
2088
|
+
* promote/demote those links. Replaces "an agent read the log and said it
|
|
2089
|
+
* passed" with something auditable back to a specific run.
|
|
2090
|
+
*
|
|
2091
|
+
* A malformed report exits non-zero having changed nothing (A4).
|
|
2092
|
+
*/
|
|
2093
|
+
program
|
|
2094
|
+
.command('verify')
|
|
2095
|
+
.description('Ingest a test report (vitest JSON) and promote/demote spec test links from it')
|
|
2096
|
+
.requiredOption('--report <file>', 'Path to the test report (vitest `--reporter=json` output)')
|
|
2097
|
+
.option('-p, --path <path>', 'Project path')
|
|
2098
|
+
.option('-j, --json', 'Output as JSON')
|
|
2099
|
+
.option('--run-id <id>', 'Identifier recorded as the run behind the verdict')
|
|
2100
|
+
.action(async (options) => {
|
|
2101
|
+
const projectPath = resolveProjectPath(options.path);
|
|
2102
|
+
try {
|
|
2103
|
+
if (!(0, directory_1.isInitialized)(projectPath)) {
|
|
2104
|
+
error(`SpecShip not initialized in ${projectPath}`);
|
|
2105
|
+
process.exit(1);
|
|
2106
|
+
}
|
|
2107
|
+
const { default: SpecShip } = await loadSpecShip();
|
|
2108
|
+
const cg = await SpecShip.open(projectPath);
|
|
2109
|
+
let result;
|
|
2110
|
+
try {
|
|
2111
|
+
result = cg.verifyFromReport(options.report, { runId: options.runId });
|
|
2112
|
+
}
|
|
2113
|
+
catch (err) {
|
|
2114
|
+
cg.destroy();
|
|
2115
|
+
error(`verify failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2116
|
+
process.exit(1);
|
|
2117
|
+
return;
|
|
2118
|
+
}
|
|
2119
|
+
if (options.json) {
|
|
2120
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2121
|
+
}
|
|
2122
|
+
else {
|
|
2123
|
+
success(`Read ${result.cases} test case(s) from ${result.reportPath} (run ${result.runId}).`);
|
|
2124
|
+
for (const p of result.promoted) {
|
|
2125
|
+
console.log(` ${chalk.green('✔')} ${p.specId} ${chalk.dim(`${p.from} → verified`)} — ${p.titles[0] ?? ''}`);
|
|
2126
|
+
}
|
|
2127
|
+
for (const d of result.demoted) {
|
|
2128
|
+
console.log(` ${chalk.red('✗')} ${d.specId} ${chalk.dim(`${d.from} → broken`)} — ${d.titles[0] ?? ''}`);
|
|
2129
|
+
}
|
|
2130
|
+
console.log();
|
|
2131
|
+
info(`${result.promoted.length} verified, ${result.demoted.length} broken, ${result.unchanged} unchanged, ${result.unmatched} unmatched.`);
|
|
2132
|
+
// Unmatched cases are reported, never silently dropped (A3).
|
|
2133
|
+
for (const u of result.unmatchedSample)
|
|
2134
|
+
console.log(chalk.dim(` unmatched: ${u}`));
|
|
2135
|
+
if (result.unmatched > result.unmatchedSample.length) {
|
|
2136
|
+
console.log(chalk.dim(` …and ${result.unmatched - result.unmatchedSample.length} more`));
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
cg.destroy();
|
|
2140
|
+
}
|
|
2141
|
+
catch (err) {
|
|
2142
|
+
error(`verify failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2143
|
+
process.exit(1);
|
|
2144
|
+
}
|
|
2145
|
+
});
|
|
1723
2146
|
/**
|
|
1724
2147
|
* specship fitness
|
|
1725
2148
|
*
|
|
@@ -1800,6 +2223,7 @@ function main() {
|
|
|
1800
2223
|
.option('-j, --json', 'Output as JSON')
|
|
1801
2224
|
.option('--strict', 'Treat every check as gating for this run only (nothing read from or written to config)')
|
|
1802
2225
|
.option('--enable-gate <checks...>', 'Persist gating for the named checks into specship.config.json, then run the gate')
|
|
2226
|
+
.option('--since <ref>', 'Evaluate only requirements whose links or spec files touch files changed since <ref> (drift + behaviour; fitness and maintainability stay repo-global)')
|
|
1803
2227
|
.action(async (pathArg, options) => {
|
|
1804
2228
|
const projectPath = resolveProjectPath(pathArg);
|
|
1805
2229
|
try {
|
|
@@ -1808,6 +2232,21 @@ function main() {
|
|
|
1808
2232
|
process.exit(1);
|
|
1809
2233
|
}
|
|
1810
2234
|
const enforce = await Promise.resolve().then(() => __importStar(require('../enforce/enforce')));
|
|
2235
|
+
// --since narrows the link-scoped checks (REQ-AUTHG-006.A1). An
|
|
2236
|
+
// unresolvable ref exits non-zero here: falling back to a full-repo run
|
|
2237
|
+
// would answer a question the caller did not ask and hide the typo
|
|
2238
|
+
// (A3).
|
|
2239
|
+
let scope;
|
|
2240
|
+
if (options.since) {
|
|
2241
|
+
const { changedFilesSince } = await Promise.resolve().then(() => __importStar(require('../enforce/changed-files')));
|
|
2242
|
+
try {
|
|
2243
|
+
scope = { changedFiles: changedFilesSince(projectPath, options.since), since: options.since };
|
|
2244
|
+
}
|
|
2245
|
+
catch (err) {
|
|
2246
|
+
error(err instanceof Error ? err.message : String(err));
|
|
2247
|
+
process.exit(1);
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
1811
2250
|
// --enable-gate writes the opt-in config, then falls through to a
|
|
1812
2251
|
// normal run so the user immediately sees the gate with teeth
|
|
1813
2252
|
// (REQ-ENFORCE-004.A1).
|
|
@@ -1825,17 +2264,27 @@ function main() {
|
|
|
1825
2264
|
const { default: SpecShip } = await loadSpecShip();
|
|
1826
2265
|
const cg = await SpecShip.open(projectPath);
|
|
1827
2266
|
// --strict gates everything for this run only (REQ-ENFORCE-004.A2).
|
|
1828
|
-
const r = options.strict
|
|
2267
|
+
const r = options.strict
|
|
2268
|
+
? cg.getEnforce(enforce.strictEnforceConfig(), scope)
|
|
2269
|
+
: cg.getEnforce(undefined, scope);
|
|
1829
2270
|
if (options.json) {
|
|
1830
2271
|
console.log(JSON.stringify(r, null, 2));
|
|
1831
2272
|
cg.destroy();
|
|
1832
2273
|
process.exit(r.passed ? 0 : 1);
|
|
1833
2274
|
}
|
|
1834
2275
|
console.log(chalk.bold('\nEnforcement gate\n'));
|
|
2276
|
+
if (scope) {
|
|
2277
|
+
console.log(chalk.dim(` scoped to ${scope.changedFiles.length} file(s) changed since ${scope.since}` +
|
|
2278
|
+
' — drift and behaviour are link-scoped; fitness and maintainability are repo-global analyses and ran unscoped.\n'));
|
|
2279
|
+
}
|
|
1835
2280
|
for (const c of r.checks) {
|
|
1836
2281
|
const tag = c.gating ? chalk.dim('[gating]') : chalk.dim('[advisory]');
|
|
1837
|
-
|
|
1838
|
-
|
|
2282
|
+
// A skipped check's input was unavailable — presented as skipped,
|
|
2283
|
+
// never as pass or fail (REQ-QGATE-002.A3).
|
|
2284
|
+
const mark = c.skipped ? chalk.yellow('○') : c.passed ? chalk.green('✓') : (c.gating ? chalk.red('✗') : chalk.yellow('•'));
|
|
2285
|
+
const reach = scope ? chalk.dim(c.scoped ? ' [since]' : ' [repo-wide]') : '';
|
|
2286
|
+
const status = c.skipped ? 'skipped' : c.passed ? 'pass' : `${c.findings.length} finding(s)`;
|
|
2287
|
+
console.log(` ${mark} ${c.check.padEnd(16)} ${tag}${reach} ${status}`);
|
|
1839
2288
|
if (!c.passed)
|
|
1840
2289
|
for (const f of c.findings.slice(0, 8))
|
|
1841
2290
|
console.log(chalk.dim(` ${f}`));
|
|
@@ -1849,7 +2298,7 @@ function main() {
|
|
|
1849
2298
|
: 'Pass (no gating checks failed).');
|
|
1850
2299
|
// The advisory sell (REQ-ENFORCE-004.A1): findings that would fail
|
|
1851
2300
|
// a gated run end the report with the exact opt-in command.
|
|
1852
|
-
const wouldGate = r.checks.filter((c) => !c.gating && !c.passed).map((c) => c.check);
|
|
2301
|
+
const wouldGate = r.checks.filter((c) => !c.gating && !c.passed && !c.skipped).map((c) => c.check);
|
|
1853
2302
|
if (wouldGate.length) {
|
|
1854
2303
|
const total = r.checks
|
|
1855
2304
|
.filter((c) => wouldGate.includes(c.check))
|
|
@@ -1974,6 +2423,39 @@ function main() {
|
|
|
1974
2423
|
process.exit(1);
|
|
1975
2424
|
}
|
|
1976
2425
|
}
|
|
2426
|
+
program
|
|
2427
|
+
.command('suppressions [path]')
|
|
2428
|
+
.description('Scan the change under review for newly introduced lint/type suppression comments (noqa, eslint-disable, @ts-ignore, …); exits non-zero when any were added')
|
|
2429
|
+
.option('--since <ref>', 'Diff base (default HEAD): scan lines added since this ref')
|
|
2430
|
+
.option('-j, --json', 'Output findings as JSON')
|
|
2431
|
+
.action(async (pathArg, options) => {
|
|
2432
|
+
const projectPath = resolveProjectPath(pathArg);
|
|
2433
|
+
// The SUPPRESSION_GATE marker is the machine-readable contract the
|
|
2434
|
+
// spec-implement workflows route on (REQ-QGATE-004.A3) — same doctrine as
|
|
2435
|
+
// VERIFY_RESULT: a scan that could not run is `skipped`, never `clean`.
|
|
2436
|
+
try {
|
|
2437
|
+
const { workingTreeDiffText } = await Promise.resolve().then(() => __importStar(require('../enforce/changed-files')));
|
|
2438
|
+
const { scanDisableComments, formatSuppressionFinding } = await Promise.resolve().then(() => __importStar(require('../enforce/suppressions')));
|
|
2439
|
+
const findings = scanDisableComments(workingTreeDiffText(projectPath, options.since ?? 'HEAD'));
|
|
2440
|
+
if (options.json) {
|
|
2441
|
+
console.log(JSON.stringify({ gate: findings.length === 0 ? 'clean' : 'failed', findings }, null, 2));
|
|
2442
|
+
process.exit(findings.length === 0 ? 0 : 1);
|
|
2443
|
+
}
|
|
2444
|
+
if (findings.length === 0) {
|
|
2445
|
+
console.log('SUPPRESSION_GATE=clean');
|
|
2446
|
+
return;
|
|
2447
|
+
}
|
|
2448
|
+
console.log('SUPPRESSION_GATE=failed');
|
|
2449
|
+
for (const f of findings)
|
|
2450
|
+
console.log(` ${formatSuppressionFinding(f)}`);
|
|
2451
|
+
console.log('Fix the code instead of silencing the rule (REQ-QGATE-004).');
|
|
2452
|
+
process.exit(1);
|
|
2453
|
+
}
|
|
2454
|
+
catch (err) {
|
|
2455
|
+
console.log('SUPPRESSION_GATE=skipped');
|
|
2456
|
+
console.log(`SUPPRESSION_REASON=${err instanceof Error ? err.message.split('\n')[0] : String(err)}`);
|
|
2457
|
+
}
|
|
2458
|
+
});
|
|
1977
2459
|
program
|
|
1978
2460
|
.command('serve')
|
|
1979
2461
|
.description('Start SpecShip as an MCP server for AI assistants (stdio transport)')
|
|
@@ -2458,16 +2940,16 @@ function main() {
|
|
|
2458
2940
|
.command('install')
|
|
2459
2941
|
.description('Install specship MCP server into Claude Code')
|
|
2460
2942
|
.option('-l, --location <where>', 'Install location: "global" or "local". Default: prompt (local)')
|
|
2461
|
-
.option('-y, --yes', 'Non-interactive: defaults to --location=local
|
|
2462
|
-
.option('--no-permissions', 'Skip writing the auto-allow permissions list')
|
|
2943
|
+
.option('-y, --yes', 'Non-interactive: defaults to --location=local (the only question an interactive install asks)')
|
|
2944
|
+
.option('--no-permissions', 'Skip writing the auto-allow permissions list (written by default)')
|
|
2463
2945
|
.option('--sdd', '(default) Install the spec-driven-development governance tier (spec/authoring/review/design commands + the spec-author nudge hook)')
|
|
2464
2946
|
.option('--no-sdd', 'Skip the governance tier — retrieval-only install (the pre-0.18 default)')
|
|
2465
2947
|
.option('--path <repo>', 'Target repo to wire and initialize (default: current directory). Project-local files and the .specship index land there')
|
|
2466
2948
|
.option('--with-jira', 'Enable the optional JIRA integration (talks to your Atlassian instance; off by default — the core install is 100% local)')
|
|
2467
2949
|
.option('--with-designer', 'Enable the optional Designer integration (EXPERIMENTAL — drives claude.ai/design via a debug Chrome session and may break without notice; off by default)')
|
|
2468
|
-
.option('--statusline', 'Wire the SpecShip status-line segment into Claude
|
|
2469
|
-
.option('--skip-statusline', 'Do not add the status-line segment (
|
|
2470
|
-
.option('--skip-index', 'Do not
|
|
2950
|
+
.option('--statusline', '(default) Wire the SpecShip status-line segment into Claude; never overwrites an existing status line')
|
|
2951
|
+
.option('--skip-statusline', 'Do not add the status-line segment (it is added by default)')
|
|
2952
|
+
.option('--skip-index', 'Do not index the current project (it is indexed by default)')
|
|
2471
2953
|
.option('--print-config', 'Print the MCP config snippet and exit (no file writes). Claude Code by default; `--target gemini` prints the Gemini CLI settings snippet')
|
|
2472
2954
|
// -t/--target SELECTS the agents to wire (GEMINI-TARGET-DOC, REQ-GEMINI-002)
|
|
2473
2955
|
// and also picks the snippet --print-config prints. The legacy spellings
|
|
@@ -2475,6 +2957,7 @@ function main() {
|
|
|
2475
2957
|
// existing invocations — including our own offline-install scripts — behave
|
|
2476
2958
|
// exactly as before.
|
|
2477
2959
|
.option('-t, --target <ids>', 'Agents to wire, comma-separated: "claude" (default) | "gemini" | "claude,gemini". Also selects the snippet `--print-config` prints. Legacy: "auto"/"all" = claude, "none" = skip')
|
|
2960
|
+
.option('--trust', 'Gemini only (REQ-GEMINI-009): write ~/.gemini/policies/specship.toml auto-allowing SpecShip\'s read-only tools. Opt-in — an admin policy still wins and run_shell_command is unaffected')
|
|
2478
2961
|
.action(async (opts) => {
|
|
2479
2962
|
if (opts.printConfig) {
|
|
2480
2963
|
const loc = (opts.location === 'local' ? 'local' : 'global');
|
|
@@ -2509,30 +2992,23 @@ function main() {
|
|
|
2509
2992
|
}
|
|
2510
2993
|
try {
|
|
2511
2994
|
// Commander's `--no-permissions` makes `opts.permissions === false`;
|
|
2512
|
-
// omitting the flag leaves it `true` (the positive-form default)
|
|
2513
|
-
//
|
|
2514
|
-
//
|
|
2515
|
-
//
|
|
2516
|
-
// prompt is silently skipped on every interactive run.
|
|
2995
|
+
// omitting the flag leaves it `true` (the positive-form default), which
|
|
2996
|
+
// we must forward as `undefined` — "user did not override" — so the
|
|
2997
|
+
// installer can tell a default from an explicit choice and name the
|
|
2998
|
+
// defaults in its summary (REQ-SLIM-003.A2).
|
|
2517
2999
|
const explicitNoPermissions = opts.permissions === false;
|
|
2518
|
-
const autoAllow = explicitNoPermissions
|
|
3000
|
+
const autoAllow = explicitNoPermissions ? false : undefined;
|
|
3001
|
+
// Status line: default ON, never prompted (REQ-SLIM-003.A1).
|
|
3002
|
+
// `--skip-statusline` is the opt-out; `--statusline` is now a no-op
|
|
3003
|
+
// affirmation kept for compatibility (REQ-SLIM-003.A3). Distinct flag
|
|
3004
|
+
// names (rather than `--no-statusline`) dodge commander's `--no-*`
|
|
3005
|
+
// default-true coupling.
|
|
3006
|
+
const statusLine = opts.skipStatusline
|
|
2519
3007
|
? false
|
|
2520
|
-
: opts.
|
|
3008
|
+
: opts.statusline
|
|
2521
3009
|
? true
|
|
2522
3010
|
: undefined;
|
|
2523
|
-
|
|
2524
|
-
// forces off (both skip the prompt); neither ⇒ undefined ⇒ ask
|
|
2525
|
-
// interactively (default no). Distinct flag names dodge commander's
|
|
2526
|
-
// `--no-*` default-true coupling that would auto-install.
|
|
2527
|
-
const statusLine = opts.statusline
|
|
2528
|
-
? true
|
|
2529
|
-
: opts.skipStatusline
|
|
2530
|
-
? false
|
|
2531
|
-
: undefined;
|
|
2532
|
-
// The governance tier is opt-in (INSTALL-WEDGE-DOC): `--sdd` makes
|
|
2533
|
-
// `opts.sdd === true`; omitting it leaves it undefined → retrieval-only.
|
|
2534
|
-
// Forward `true` only when the user explicitly opted in.
|
|
2535
|
-
await runInstallerWithOptions({
|
|
3011
|
+
const outcome = await runInstallerWithOptions({
|
|
2536
3012
|
target: opts.target,
|
|
2537
3013
|
location: opts.location,
|
|
2538
3014
|
autoAllow,
|
|
@@ -2542,10 +3018,16 @@ function main() {
|
|
|
2542
3018
|
withJira: opts.withJira === true ? true : undefined,
|
|
2543
3019
|
withDesigner: opts.withDesigner === true ? true : undefined,
|
|
2544
3020
|
statusLine,
|
|
3021
|
+
// REQ-AUDFIX-001: a local install must honor --skip-index too.
|
|
3022
|
+
skipIndex: opts.skipIndex === true,
|
|
3023
|
+
// REQ-GEMINI-009: strictly opt-in, no prompt. Claude target ignores it.
|
|
3024
|
+
trust: opts.trust === true ? true : undefined,
|
|
2545
3025
|
yes: opts.yes,
|
|
2546
3026
|
});
|
|
2547
|
-
//
|
|
2548
|
-
//
|
|
3027
|
+
// Index the current project (REQ-HANDSHAKE-004) before the smoke check,
|
|
3028
|
+
// so the new index is reflected by the index-queryable item. Not a
|
|
3029
|
+
// question any more (REQ-SLIM-003.A1) — `--skip-index` opts out, and the
|
|
3030
|
+
// summary below names the default.
|
|
2549
3031
|
const cwd = process.cwd();
|
|
2550
3032
|
const { decideInstallInit } = await Promise.resolve().then(() => __importStar(require('../installer/init-offer')));
|
|
2551
3033
|
const { isGitRepo } = await Promise.resolve().then(() => __importStar(require('../sync/git-hooks')));
|
|
@@ -2555,14 +3037,7 @@ function main() {
|
|
|
2555
3037
|
yes: opts.yes === true,
|
|
2556
3038
|
skipIndex: opts.skipIndex === true,
|
|
2557
3039
|
});
|
|
2558
|
-
|
|
2559
|
-
if (initDecision === 'offer') {
|
|
2560
|
-
const clack = await importESM('@clack/prompts');
|
|
2561
|
-
const ans = await clack.confirm({
|
|
2562
|
-
message: `Index this project (${cwd}) now, so Claude can explore it?`,
|
|
2563
|
-
});
|
|
2564
|
-
doIndex = ans === true; // a cancel (symbol) or "no" both decline (REQ-HANDSHAKE-004.A2)
|
|
2565
|
-
}
|
|
3040
|
+
const doIndex = initDecision === 'auto-index';
|
|
2566
3041
|
if (doIndex) {
|
|
2567
3042
|
console.log();
|
|
2568
3043
|
info(`Indexing ${cwd} …`);
|
|
@@ -2574,6 +3049,22 @@ function main() {
|
|
|
2574
3049
|
warn(`Indexing failed (run \`specship init -i\` later): ${e instanceof Error ? e.message : String(e)}`);
|
|
2575
3050
|
}
|
|
2576
3051
|
}
|
|
3052
|
+
// Name the defaulted indexing decision the same way the installer names
|
|
3053
|
+
// its own (REQ-SLIM-003.A2). The installer already printed this line when
|
|
3054
|
+
// a local install built the index itself, so it is never doubled.
|
|
3055
|
+
if (doIndex && !outcome?.indexed) {
|
|
3056
|
+
const { describeInstallDefaults } = await Promise.resolve().then(() => __importStar(require('../installer/install-defaults')));
|
|
3057
|
+
for (const line of describeInstallDefaults({
|
|
3058
|
+
statusLine: 'skipped',
|
|
3059
|
+
statusLineDefaulted: false,
|
|
3060
|
+
autoAllow: false,
|
|
3061
|
+
autoAllowDefaulted: false,
|
|
3062
|
+
index: 'indexed',
|
|
3063
|
+
indexDefaulted: true,
|
|
3064
|
+
})) {
|
|
3065
|
+
info(line);
|
|
3066
|
+
}
|
|
3067
|
+
}
|
|
2577
3068
|
// Post-install smoke check (REQ-HANDSHAKE-002). Advisory: report failing
|
|
2578
3069
|
// items but never exit non-zero (REQ-HANDSHAKE-002.A4), so a provisioning
|
|
2579
3070
|
// script is never broken by it — the gating equivalent is `specship doctor`.
|
|
@@ -2707,7 +3198,32 @@ function main() {
|
|
|
2707
3198
|
}
|
|
2708
3199
|
}
|
|
2709
3200
|
const { buildCheatsheetPayload } = await Promise.resolve().then(() => __importStar(require('../activation/cheatsheet')));
|
|
2710
|
-
|
|
3201
|
+
// Learned-state extras (REQ-ALEARN-002 / REQ-ALEARN-004.A2) — both
|
|
3202
|
+
// best-effort: an unreadable file or DB silently yields the base
|
|
3203
|
+
// cheatsheet, never an error in the SessionStart hook.
|
|
3204
|
+
const projectPath = resolveProjectPath(undefined);
|
|
3205
|
+
let conventions = null;
|
|
3206
|
+
try {
|
|
3207
|
+
conventions = (await Promise.resolve().then(() => __importStar(require('fs')))).readFileSync((await Promise.resolve().then(() => __importStar(require('path')))).join(projectPath, '.specship', 'conventions.md'), 'utf-8');
|
|
3208
|
+
}
|
|
3209
|
+
catch { /* no conventions file */ }
|
|
3210
|
+
let openProposals = 0;
|
|
3211
|
+
try {
|
|
3212
|
+
const { DatabaseConnection, DATABASE_FILENAME } = await Promise.resolve().then(() => __importStar(require('../db')));
|
|
3213
|
+
const { ReflectStore } = await Promise.resolve().then(() => __importStar(require('../reflect')));
|
|
3214
|
+
const dbPath = (await Promise.resolve().then(() => __importStar(require('path')))).join(projectPath, '.specship', DATABASE_FILENAME);
|
|
3215
|
+
if ((await Promise.resolve().then(() => __importStar(require('fs')))).existsSync(dbPath)) {
|
|
3216
|
+
const conn = DatabaseConnection.initialize(dbPath);
|
|
3217
|
+
try {
|
|
3218
|
+
openProposals = new ReflectStore(conn.getDb()).list('open').length;
|
|
3219
|
+
}
|
|
3220
|
+
finally {
|
|
3221
|
+
conn.close();
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
3224
|
+
}
|
|
3225
|
+
catch { /* store unavailable → omit the line */ }
|
|
3226
|
+
const payload = buildCheatsheetPayload(process.env, { conventions, openProposals });
|
|
2711
3227
|
if (payload)
|
|
2712
3228
|
process.stdout.write(JSON.stringify(payload) + '\n');
|
|
2713
3229
|
process.exit(0);
|
|
@@ -2745,21 +3261,14 @@ function main() {
|
|
|
2745
3261
|
}
|
|
2746
3262
|
catch { /* no stdin — nothing to nudge */ }
|
|
2747
3263
|
const raw = Buffer.concat(chunks).toString('utf-8').trim();
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
prompt = typeof parsed?.prompt === 'string' ? parsed.prompt : raw;
|
|
2753
|
-
}
|
|
2754
|
-
catch {
|
|
2755
|
-
prompt = raw; // not JSON — treat as the raw prompt text
|
|
2756
|
-
}
|
|
2757
|
-
}
|
|
2758
|
-
if (shouldNudge(prompt)) {
|
|
3264
|
+
const { parseHookPayload, buildHookAdditionalContext, deriveHostTraits } = await Promise.resolve().then(() => __importStar(require('../activation/hook-output')));
|
|
3265
|
+
const payload = parseHookPayload(raw);
|
|
3266
|
+
const traits = deriveHostTraits(payload);
|
|
3267
|
+
if (shouldNudge(payload.prompt || raw)) {
|
|
2759
3268
|
const additionalContext = 'This repo uses spec-driven development (SpecShip). Before any brainstorming or ' +
|
|
2760
3269
|
'planning skill, FIRST invoke spec-author to author the spec under specs/ for this ' +
|
|
2761
3270
|
'work — the spec is the contract; implement from it with /specship:spec implement.';
|
|
2762
|
-
process.stdout.write(
|
|
3271
|
+
process.stdout.write(buildHookAdditionalContext(additionalContext, payload.hookEventName, traits.fallbackPromptEvent));
|
|
2763
3272
|
}
|
|
2764
3273
|
process.exit(0);
|
|
2765
3274
|
});
|
|
@@ -2775,11 +3284,9 @@ function main() {
|
|
|
2775
3284
|
.command('steer-nudge')
|
|
2776
3285
|
.description('Internal hook: steer flow/structure questions to specship_explore (UserPromptSubmit)')
|
|
2777
3286
|
.action(async () => {
|
|
2778
|
-
// Prefer the hook payload's cwd (Claude Code
|
|
2779
|
-
// check targets the project the prompt belongs to, not
|
|
2780
|
-
// process happened to spawn.
|
|
2781
|
-
let cwd = process.cwd();
|
|
2782
|
-
let transcriptPath = null;
|
|
3287
|
+
// Prefer the hook payload's cwd (Claude Code / Gemini CLI both send it
|
|
3288
|
+
// on stdin) so the check targets the project the prompt belongs to, not
|
|
3289
|
+
// wherever the hook process happened to spawn.
|
|
2783
3290
|
const chunks = [];
|
|
2784
3291
|
try {
|
|
2785
3292
|
for await (const c of process.stdin)
|
|
@@ -2787,81 +3294,100 @@ function main() {
|
|
|
2787
3294
|
}
|
|
2788
3295
|
catch { /* no stdin — fall back to process.cwd() */ }
|
|
2789
3296
|
const raw = Buffer.concat(chunks).toString('utf-8').trim();
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
cwd = parsed.cwd;
|
|
2795
|
-
if (typeof parsed?.transcript_path === 'string' && parsed.transcript_path.length > 0) {
|
|
2796
|
-
transcriptPath = parsed.transcript_path;
|
|
2797
|
-
}
|
|
2798
|
-
}
|
|
2799
|
-
catch { /* not JSON — keep process.cwd() */ }
|
|
2800
|
-
}
|
|
3297
|
+
const { parseHookPayload, buildHookAdditionalContext, deriveHostTraits } = await Promise.resolve().then(() => __importStar(require('../activation/hook-output')));
|
|
3298
|
+
const payload = parseHookPayload(raw);
|
|
3299
|
+
const traits = deriveHostTraits(payload);
|
|
3300
|
+
const cwd = payload.cwd ?? process.cwd();
|
|
2801
3301
|
// Record the session's model from the transcript tail (MODCTX-DOC,
|
|
2802
3302
|
// REQ-MODCTX-001.A4) — the primary detection channel, present on every
|
|
2803
3303
|
// default install. Fires per prompt so mid-session /model switches
|
|
2804
3304
|
// track; runs even when the steering text is suppressed, but only in
|
|
2805
3305
|
// initialized projects. Best-effort: never fails the hook.
|
|
2806
|
-
if (transcriptPath && fs.existsSync(path.join(cwd, '.specship'))) {
|
|
3306
|
+
if (payload.transcriptPath && fs.existsSync(path.join(cwd, '.specship'))) {
|
|
2807
3307
|
try {
|
|
2808
3308
|
const { readModelFromTranscript, recordSessionModel } = await Promise.resolve().then(() => __importStar(require('../mcp/model-context')));
|
|
2809
|
-
const model = readModelFromTranscript(transcriptPath);
|
|
3309
|
+
const model = readModelFromTranscript(payload.transcriptPath);
|
|
2810
3310
|
if (model)
|
|
2811
3311
|
recordSessionModel(cwd, model);
|
|
2812
3312
|
}
|
|
2813
3313
|
catch { /* telemetry must never break the hook */ }
|
|
2814
3314
|
}
|
|
2815
3315
|
const { buildSteeringNudge } = await Promise.resolve().then(() => __importStar(require('../activation/steering')));
|
|
2816
|
-
const additionalContext = buildSteeringNudge(cwd);
|
|
3316
|
+
const additionalContext = buildSteeringNudge(cwd, process.env, undefined, traits.toolPrefix);
|
|
2817
3317
|
if (additionalContext) {
|
|
2818
|
-
process.stdout.write(
|
|
3318
|
+
process.stdout.write(buildHookAdditionalContext(additionalContext, payload.hookEventName, traits.fallbackPromptEvent));
|
|
2819
3319
|
}
|
|
2820
3320
|
process.exit(0);
|
|
2821
3321
|
});
|
|
2822
3322
|
/**
|
|
2823
|
-
* specship search-intercept (STEER-HOOK-DOC, REQ-STEER-004/005)
|
|
3323
|
+
* specship search-intercept (STEER-HOOK-DOC, REQ-STEER-004/005, REQ-GEMINI-011)
|
|
2824
3324
|
*
|
|
2825
|
-
* Internal
|
|
2826
|
-
*
|
|
2827
|
-
*
|
|
2828
|
-
*
|
|
2829
|
-
*
|
|
2830
|
-
*
|
|
3325
|
+
* Internal hook on search-shaped tools. For Claude's PreToolUse (Grep/Glob)
|
|
3326
|
+
* and Gemini's BeforeTool (grep_search / glob / run_shell_command), emit the
|
|
3327
|
+
* point-of-use redirect to specship_explore. Host-aware traits are derived
|
|
3328
|
+
* from the payload in `activation/hook-output.ts`; shared code never sniffs
|
|
3329
|
+
* event names. Advisory for Claude; Gemini also supports strict mode
|
|
3330
|
+
* (`SPECSHIP_GEMINI_STRICT=1`) which denies grep_search / glob — never
|
|
3331
|
+
* run_shell_command (REQ-GEMINI-011.A3). Exits 0 on every path.
|
|
2831
3332
|
*/
|
|
2832
3333
|
program
|
|
2833
3334
|
.command('search-intercept')
|
|
2834
|
-
.description('Internal hook: point-of-use redirect from search tools to specship_explore (PreToolUse)')
|
|
3335
|
+
.description('Internal hook: point-of-use redirect from search tools to specship_explore (PreToolUse / BeforeTool)')
|
|
2835
3336
|
.action(async () => {
|
|
2836
3337
|
try {
|
|
2837
|
-
let cwd = process.cwd();
|
|
2838
|
-
let sessionId = null;
|
|
2839
|
-
let transcriptPath = null;
|
|
2840
3338
|
const chunks = [];
|
|
2841
3339
|
try {
|
|
2842
3340
|
for await (const c of process.stdin)
|
|
2843
3341
|
chunks.push(c);
|
|
2844
3342
|
}
|
|
2845
|
-
catch { /* no stdin — fall
|
|
2846
|
-
const raw = Buffer.concat(chunks).toString('utf-8')
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
3343
|
+
catch { /* no stdin — fall through with empty payload */ }
|
|
3344
|
+
const raw = Buffer.concat(chunks).toString('utf-8');
|
|
3345
|
+
const { parseHookPayload, deriveHostTraits, buildHookAdditionalContext, buildHookDenyDecision } = await Promise.resolve().then(() => __importStar(require('../activation/hook-output')));
|
|
3346
|
+
const payload = parseHookPayload(raw);
|
|
3347
|
+
const traits = deriveHostTraits(payload);
|
|
3348
|
+
const cwd = payload.cwd ?? process.cwd();
|
|
3349
|
+
let toolName = '';
|
|
3350
|
+
let toolInput = {};
|
|
3351
|
+
try {
|
|
3352
|
+
const parsed = JSON.parse(raw);
|
|
3353
|
+
if (typeof parsed?.tool_name === 'string')
|
|
3354
|
+
toolName = parsed.tool_name;
|
|
3355
|
+
if (parsed?.tool_input && typeof parsed.tool_input === 'object' && !Array.isArray(parsed.tool_input)) {
|
|
3356
|
+
toolInput = parsed.tool_input;
|
|
2858
3357
|
}
|
|
2859
|
-
catch { /* not JSON — keep defaults */ }
|
|
2860
3358
|
}
|
|
3359
|
+
catch { /* not JSON — strict path stays off, advisory still works via payload */ }
|
|
3360
|
+
const DENY_TOOLS = new Set(traits.denyEligibleTools);
|
|
3361
|
+
const shellCommand = traits.shellTool !== null && toolName === traits.shellTool && typeof toolInput.command === 'string'
|
|
3362
|
+
? toolInput.command
|
|
3363
|
+
: null;
|
|
3364
|
+
const { resolveSetting } = await Promise.resolve().then(() => __importStar(require('../config/runtime-settings')));
|
|
3365
|
+
const strict = traits.supportsBeforeToolDeny &&
|
|
3366
|
+
resolveSetting('SPECSHIP_GEMINI_STRICT', cwd, process.env, os.homedir()) === '1';
|
|
3367
|
+
const searchPattern = (typeof toolInput.pattern === 'string' && toolInput.pattern) ||
|
|
3368
|
+
(typeof toolInput.query === 'string' && toolInput.query) ||
|
|
3369
|
+
null;
|
|
2861
3370
|
const { buildSearchIntercept } = await Promise.resolve().then(() => __importStar(require('../activation/search-intercept')));
|
|
2862
|
-
const
|
|
2863
|
-
|
|
2864
|
-
|
|
3371
|
+
const result = buildSearchIntercept({
|
|
3372
|
+
cwd,
|
|
3373
|
+
sessionId: payload.sessionId,
|
|
3374
|
+
transcriptPath: payload.transcriptPath,
|
|
3375
|
+
specshipToolMarkers: traits.specshipToolMarkers,
|
|
3376
|
+
transcriptFormat: traits.transcriptFormat,
|
|
3377
|
+
strict,
|
|
3378
|
+
denyEligible: DENY_TOOLS.has(toolName),
|
|
3379
|
+
shellCommand,
|
|
3380
|
+
searchPattern,
|
|
3381
|
+
});
|
|
3382
|
+
if (result) {
|
|
3383
|
+
if ('decision' in result) {
|
|
3384
|
+
// Gemini BeforeToolHookOutput reads `decision` at the top level
|
|
3385
|
+
// (isBlockingDecision), not inside hookSpecificOutput.
|
|
3386
|
+
process.stdout.write(buildHookDenyDecision(result.reason));
|
|
3387
|
+
}
|
|
3388
|
+
else {
|
|
3389
|
+
process.stdout.write(buildHookAdditionalContext(result.additionalContext, payload.hookEventName, traits.fallbackToolEvent));
|
|
3390
|
+
}
|
|
2865
3391
|
}
|
|
2866
3392
|
}
|
|
2867
3393
|
catch { /* advisory only — never block the matched call (REQ-STEER-004.A2) */ }
|
|
@@ -2963,6 +3489,75 @@ function main() {
|
|
|
2963
3489
|
cg.close();
|
|
2964
3490
|
}
|
|
2965
3491
|
});
|
|
3492
|
+
// @implements REQ-PIPETRUST-007
|
|
3493
|
+
// Pipeline health: release run, npm lag, last JIRA auto-publish, merged run
|
|
3494
|
+
// worktrees. Network checks are time-bounded and skip offline; results are
|
|
3495
|
+
// cached to .specship/pulse.json for the session-start summary.
|
|
3496
|
+
program
|
|
3497
|
+
.command('pulse [path]')
|
|
3498
|
+
.description('Report pipeline health (release run, npm lag, JIRA auto-publish, merged run worktrees); exits non-zero on any failure.')
|
|
3499
|
+
.option('--refresh', 'ignore the cache and re-run every check')
|
|
3500
|
+
.option('--max-age <minutes>', 'serve the cached result when younger than this (default: always re-run)')
|
|
3501
|
+
.option('--json', 'emit JSON')
|
|
3502
|
+
.action(async (pathArg, options) => {
|
|
3503
|
+
const projectRoot = path.resolve(pathArg ?? process.cwd());
|
|
3504
|
+
const { computePulse } = await Promise.resolve().then(() => __importStar(require('../pulse/pulse')));
|
|
3505
|
+
// Resolve a run worktree back to its full run id for the purge hint
|
|
3506
|
+
// (best-effort; falls back to the short id from the branch name).
|
|
3507
|
+
let runIdForWorktree;
|
|
3508
|
+
let close;
|
|
3509
|
+
if ((0, directory_1.isInitialized)(projectRoot)) {
|
|
3510
|
+
try {
|
|
3511
|
+
const { SpecShip } = await loadSpecShip();
|
|
3512
|
+
const cg = await SpecShip.open(projectRoot);
|
|
3513
|
+
close = () => cg.close();
|
|
3514
|
+
const byPath = new Map();
|
|
3515
|
+
const sq = cg.getSpecQueries();
|
|
3516
|
+
for (const run of sq.getAllWorkflowRuns(500)) {
|
|
3517
|
+
if (!run.isolationEnvId)
|
|
3518
|
+
continue;
|
|
3519
|
+
const env = sq.getIsolationEnvById(run.isolationEnvId);
|
|
3520
|
+
if (env?.workingPath)
|
|
3521
|
+
byPath.set(path.resolve(env.workingPath), run.id);
|
|
3522
|
+
}
|
|
3523
|
+
runIdForWorktree = (p) => byPath.get(path.resolve(p));
|
|
3524
|
+
}
|
|
3525
|
+
catch {
|
|
3526
|
+
/* hint falls back to the short id */
|
|
3527
|
+
}
|
|
3528
|
+
}
|
|
3529
|
+
const maxAgeMin = options.maxAge !== undefined ? Number(options.maxAge) : NaN;
|
|
3530
|
+
let result;
|
|
3531
|
+
try {
|
|
3532
|
+
result = await computePulse({
|
|
3533
|
+
projectRoot,
|
|
3534
|
+
refresh: options.refresh,
|
|
3535
|
+
maxAgeMs: Number.isFinite(maxAgeMin) ? maxAgeMin * 60_000 : undefined,
|
|
3536
|
+
writeCache: fs.existsSync(path.join(projectRoot, '.specship')),
|
|
3537
|
+
runIdForWorktree,
|
|
3538
|
+
});
|
|
3539
|
+
}
|
|
3540
|
+
finally {
|
|
3541
|
+
close?.();
|
|
3542
|
+
}
|
|
3543
|
+
if (options.json) {
|
|
3544
|
+
// eslint-disable-next-line no-console
|
|
3545
|
+
console.log(JSON.stringify(result, null, 2));
|
|
3546
|
+
}
|
|
3547
|
+
else {
|
|
3548
|
+
const tag = { ok: 'ok ', fail: 'FAIL', skipped: 'skip' };
|
|
3549
|
+
for (const item of result.items) {
|
|
3550
|
+
// eslint-disable-next-line no-console
|
|
3551
|
+
console.log(`[${tag[item.status]}] ${item.message}${item.hint ? ` — ${item.hint}` : ''}`);
|
|
3552
|
+
}
|
|
3553
|
+
if (result.fromCache) {
|
|
3554
|
+
// eslint-disable-next-line no-console
|
|
3555
|
+
console.log(`(cached ${new Date(result.checkedAt).toISOString()}; --refresh to re-run)`);
|
|
3556
|
+
}
|
|
3557
|
+
}
|
|
3558
|
+
if (result.items.some((i) => i.status === 'fail'))
|
|
3559
|
+
process.exit(1);
|
|
3560
|
+
});
|
|
2966
3561
|
program
|
|
2967
3562
|
.command('drifted [path]')
|
|
2968
3563
|
.description('List spec links in concerning states (drifted, broken, orphaned).')
|
|
@@ -3244,14 +3839,19 @@ function main() {
|
|
|
3244
3839
|
}
|
|
3245
3840
|
});
|
|
3246
3841
|
program
|
|
3247
|
-
.command('workflow <action> [
|
|
3248
|
-
.description('Workflow engine: list | run <name> | resume <runId> | cancel <runId> | approve <runId> | reject <runId> | purge <runId> | runs')
|
|
3842
|
+
.command('workflow <action> [args...]')
|
|
3843
|
+
.description('Workflow engine: list | run <name> | run-batch <name> --specs <ids> | merge-queue <runId...> | gc | resume <runId> | cancel <runId> | approve <runId> | reject <runId> | purge <runId> | runs')
|
|
3249
3844
|
.option('-i, --input <kv...>', 'workflow inputs as KEY=VALUE (repeatable)')
|
|
3250
3845
|
.option('--path <projectRoot>', 'project root (default: cwd)')
|
|
3251
3846
|
.option('--comment <text>', 'comment for approve/reject')
|
|
3252
3847
|
.option('--reason <text>', 'reason for cancel/reject')
|
|
3848
|
+
.option('--specs <ids>', 'run-batch: comma-separated requirement ids')
|
|
3849
|
+
.option('--concurrency <n>', 'run-batch: max concurrent runs per wave (default 3)')
|
|
3850
|
+
.option('--dry-run', 'gc: list merged-run worktrees without removing them')
|
|
3851
|
+
.option('--agent <name>', 'run/run-batch: the coding agent prompt steps run on (claude | gemini; default: SPECSHIP_WORKFLOW_AGENT, workflow.agent, the launching agent, then claude)')
|
|
3253
3852
|
.option('--json', 'emit JSON where applicable')
|
|
3254
|
-
.action(async (action,
|
|
3853
|
+
.action(async (action, args, options) => {
|
|
3854
|
+
const arg = args?.[0];
|
|
3255
3855
|
const projectRoot = path.resolve(options.path ?? process.cwd());
|
|
3256
3856
|
if (!(0, directory_1.isInitialized)(projectRoot)) {
|
|
3257
3857
|
error(`SpecShip not initialized in ${projectRoot}. Run \`specship init -i\` first.`);
|
|
@@ -3259,6 +3859,7 @@ function main() {
|
|
|
3259
3859
|
}
|
|
3260
3860
|
const { SpecShip } = await loadSpecShip();
|
|
3261
3861
|
const { discoverWorkflows, loadWorkflowByName } = await Promise.resolve().then(() => __importStar(require('../workflows/discovery')));
|
|
3862
|
+
const { resolveWorkflowAgent } = await Promise.resolve().then(() => __importStar(require('../workflows/agents')));
|
|
3262
3863
|
const { WorkflowExecutor } = await Promise.resolve().then(() => __importStar(require('../workflows/executor')));
|
|
3263
3864
|
const { WorktreeProvider } = await Promise.resolve().then(() => __importStar(require('../isolation/worktree')));
|
|
3264
3865
|
const { handleJiraRunCompletion } = await Promise.resolve().then(() => __importStar(require('../mcp/jira-tools')));
|
|
@@ -3349,9 +3950,18 @@ function main() {
|
|
|
3349
3950
|
inputs[inp.name] = inp.default ?? '';
|
|
3350
3951
|
}
|
|
3351
3952
|
}
|
|
3953
|
+
// Unknown agent names fail here, before a run exists (REQ-AGENTADAPT-006.A4).
|
|
3954
|
+
try {
|
|
3955
|
+
resolveWorkflowAgent({ flag: options.agent, projectRoot });
|
|
3956
|
+
}
|
|
3957
|
+
catch (err) {
|
|
3958
|
+
error(err instanceof Error ? err.message : String(err));
|
|
3959
|
+
process.exit(1);
|
|
3960
|
+
}
|
|
3352
3961
|
const result = await executor.start(loaded.workflow, {
|
|
3353
3962
|
projectRoot,
|
|
3354
3963
|
inputs,
|
|
3964
|
+
agent: options.agent,
|
|
3355
3965
|
variables: {
|
|
3356
3966
|
ARTIFACTS_DIR: path.join((0, directory_1.getSpecShipDir)(projectRoot), 'artifacts'),
|
|
3357
3967
|
CONTEXT: projectRoot,
|
|
@@ -3472,6 +4082,208 @@ function main() {
|
|
|
3472
4082
|
: `Run ${arg} had no worktree — nothing to remove. Artifacts and the run record are kept.`);
|
|
3473
4083
|
break;
|
|
3474
4084
|
}
|
|
4085
|
+
case 'run-batch': {
|
|
4086
|
+
// REQ-THRU-001 — one run per requirement, overlapping files serialized.
|
|
4087
|
+
if (!arg) {
|
|
4088
|
+
error('workflow run-batch requires a workflow name');
|
|
4089
|
+
process.exit(1);
|
|
4090
|
+
}
|
|
4091
|
+
const loaded = loadWorkflowByName(projectRoot, arg);
|
|
4092
|
+
if (!loaded) {
|
|
4093
|
+
error(`Workflow "${arg}" not found. Use \`specship workflow list\` to see available workflows.`);
|
|
4094
|
+
process.exit(1);
|
|
4095
|
+
}
|
|
4096
|
+
const specIds = (options.specs ?? '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
4097
|
+
if (specIds.length === 0) {
|
|
4098
|
+
error('workflow run-batch requires --specs <ID,ID,...>');
|
|
4099
|
+
process.exit(1);
|
|
4100
|
+
}
|
|
4101
|
+
const concurrency = options.concurrency !== undefined ? Number(options.concurrency) : undefined;
|
|
4102
|
+
if (concurrency !== undefined && (!Number.isInteger(concurrency) || concurrency < 1)) {
|
|
4103
|
+
error('--concurrency must be a positive integer');
|
|
4104
|
+
process.exit(1);
|
|
4105
|
+
}
|
|
4106
|
+
const baseInputs = {};
|
|
4107
|
+
for (const kv of options.input ?? []) {
|
|
4108
|
+
const eqIdx = kv.indexOf('=');
|
|
4109
|
+
if (eqIdx <= 0) {
|
|
4110
|
+
error(`--input must be KEY=VALUE (got "${kv}")`);
|
|
4111
|
+
process.exit(1);
|
|
4112
|
+
}
|
|
4113
|
+
baseInputs[kv.slice(0, eqIdx)] = kv.slice(eqIdx + 1);
|
|
4114
|
+
}
|
|
4115
|
+
for (const inp of loaded.workflow.inputs ?? []) {
|
|
4116
|
+
if (inp.name !== 'SPEC_ID' && !(inp.name in baseInputs)) {
|
|
4117
|
+
if (inp.required) {
|
|
4118
|
+
error(`Required input missing: --input ${inp.name}=...`);
|
|
4119
|
+
process.exit(1);
|
|
4120
|
+
}
|
|
4121
|
+
baseInputs[inp.name] = inp.default ?? '';
|
|
4122
|
+
}
|
|
4123
|
+
}
|
|
4124
|
+
try {
|
|
4125
|
+
resolveWorkflowAgent({ flag: options.agent, projectRoot });
|
|
4126
|
+
}
|
|
4127
|
+
catch (err) {
|
|
4128
|
+
error(err instanceof Error ? err.message : String(err));
|
|
4129
|
+
process.exit(1);
|
|
4130
|
+
}
|
|
4131
|
+
const { runBatch, TERMINAL_RUN_STATUSES, DEFAULT_BATCH_CONCURRENCY } = await Promise.resolve().then(() => __importStar(require('../workflows/batch')));
|
|
4132
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
4133
|
+
let batch;
|
|
4134
|
+
try {
|
|
4135
|
+
batch = await runBatch({
|
|
4136
|
+
projectRoot,
|
|
4137
|
+
specIds,
|
|
4138
|
+
concurrency,
|
|
4139
|
+
startRun: async (specId) => {
|
|
4140
|
+
const result = await executor.start(loaded.workflow, {
|
|
4141
|
+
projectRoot,
|
|
4142
|
+
agent: options.agent,
|
|
4143
|
+
inputs: { ...baseInputs, SPEC_ID: specId },
|
|
4144
|
+
variables: {
|
|
4145
|
+
ARTIFACTS_DIR: path.join((0, directory_1.getSpecShipDir)(projectRoot), 'artifacts'),
|
|
4146
|
+
CONTEXT: projectRoot,
|
|
4147
|
+
},
|
|
4148
|
+
});
|
|
4149
|
+
return { runId: result.run.id, status: result.run.status };
|
|
4150
|
+
},
|
|
4151
|
+
// A run paused at an approval still holds its files: wait for a
|
|
4152
|
+
// terminal status (approve + resume it from another terminal).
|
|
4153
|
+
waitForRun: async (runId, status) => {
|
|
4154
|
+
let current = status;
|
|
4155
|
+
let announced = false;
|
|
4156
|
+
while (!TERMINAL_RUN_STATUSES.has(current)) {
|
|
4157
|
+
if (current === 'paused' && !announced) {
|
|
4158
|
+
// eslint-disable-next-line no-console
|
|
4159
|
+
console.log(` ${runId} is paused — approve it with \`specship workflow approve ${runId}\`, then \`specship workflow resume ${runId}\``);
|
|
4160
|
+
announced = true;
|
|
4161
|
+
}
|
|
4162
|
+
await sleep(5000);
|
|
4163
|
+
current = specQueries.getWorkflowRunById(runId)?.status ?? 'failed';
|
|
4164
|
+
}
|
|
4165
|
+
return current;
|
|
4166
|
+
},
|
|
4167
|
+
onPlan: (waves) => {
|
|
4168
|
+
if (options.json)
|
|
4169
|
+
return;
|
|
4170
|
+
// eslint-disable-next-line no-console
|
|
4171
|
+
console.log(`Serialization: ${waves.length} wave(s), concurrency ${concurrency ?? DEFAULT_BATCH_CONCURRENCY}`);
|
|
4172
|
+
waves.forEach((w, i) => {
|
|
4173
|
+
// eslint-disable-next-line no-console
|
|
4174
|
+
console.log(` wave ${i + 1}: ${w.join(', ')}`);
|
|
4175
|
+
});
|
|
4176
|
+
},
|
|
4177
|
+
onRunStarted: (r) => {
|
|
4178
|
+
if (options.json)
|
|
4179
|
+
return;
|
|
4180
|
+
// eslint-disable-next-line no-console
|
|
4181
|
+
console.log(` ${r.specId} → run ${r.runId} [${r.status}] (wave ${r.wave + 1})`);
|
|
4182
|
+
},
|
|
4183
|
+
});
|
|
4184
|
+
}
|
|
4185
|
+
catch (err) {
|
|
4186
|
+
error(err instanceof Error ? err.message : String(err));
|
|
4187
|
+
process.exit(1);
|
|
4188
|
+
}
|
|
4189
|
+
if (options.json) {
|
|
4190
|
+
// eslint-disable-next-line no-console
|
|
4191
|
+
console.log(JSON.stringify(batch, null, 2));
|
|
4192
|
+
}
|
|
4193
|
+
else {
|
|
4194
|
+
// eslint-disable-next-line no-console
|
|
4195
|
+
console.log('Runs:');
|
|
4196
|
+
for (const r of batch.runs) {
|
|
4197
|
+
// eslint-disable-next-line no-console
|
|
4198
|
+
console.log(` ${r.runId} ${r.specId} [${r.status}]`);
|
|
4199
|
+
}
|
|
4200
|
+
}
|
|
4201
|
+
if (batch.runs.some((r) => r.status === 'failed'))
|
|
4202
|
+
process.exit(1);
|
|
4203
|
+
break;
|
|
4204
|
+
}
|
|
4205
|
+
case 'merge-queue': {
|
|
4206
|
+
// REQ-THRU-002 — merge completed runs in order, re-checking each merge.
|
|
4207
|
+
const runIds = args ?? [];
|
|
4208
|
+
if (runIds.length === 0) {
|
|
4209
|
+
error('workflow merge-queue requires one or more runIds');
|
|
4210
|
+
process.exit(1);
|
|
4211
|
+
}
|
|
4212
|
+
const { runMergeQueue } = await Promise.resolve().then(() => __importStar(require('../workflows/merge-queue')));
|
|
4213
|
+
const result = runMergeQueue({
|
|
4214
|
+
projectRoot,
|
|
4215
|
+
runIds,
|
|
4216
|
+
getRun: (id) => {
|
|
4217
|
+
const run = specQueries.getWorkflowRunById(id);
|
|
4218
|
+
if (!run)
|
|
4219
|
+
return null;
|
|
4220
|
+
const env = run.isolationEnvId ? specQueries.getIsolationEnvById(run.isolationEnvId) : null;
|
|
4221
|
+
return { id: run.id, status: run.status, branch: env?.branchName };
|
|
4222
|
+
},
|
|
4223
|
+
});
|
|
4224
|
+
if (options.json) {
|
|
4225
|
+
// eslint-disable-next-line no-console
|
|
4226
|
+
console.log(JSON.stringify(result, null, 2));
|
|
4227
|
+
}
|
|
4228
|
+
else {
|
|
4229
|
+
for (const id of result.merged) {
|
|
4230
|
+
// eslint-disable-next-line no-console
|
|
4231
|
+
console.log(` merged ${id}`);
|
|
4232
|
+
}
|
|
4233
|
+
// eslint-disable-next-line no-console
|
|
4234
|
+
console.log(result.stoppedAt
|
|
4235
|
+
? `Stopped at run ${result.stoppedAt} (${result.reason}): ${result.detail ?? ''}`
|
|
4236
|
+
: `Merge queue done — ${result.merged.length} run(s) merged.`);
|
|
4237
|
+
}
|
|
4238
|
+
if (result.stoppedAt)
|
|
4239
|
+
process.exit(1);
|
|
4240
|
+
break;
|
|
4241
|
+
}
|
|
4242
|
+
case 'gc': {
|
|
4243
|
+
// REQ-THRU-007 — purge worktrees of merged runs; records + artifacts kept.
|
|
4244
|
+
const { collectMergedRuns } = await Promise.resolve().then(() => __importStar(require('../workflows/gc')));
|
|
4245
|
+
const result = collectMergedRuns({
|
|
4246
|
+
projectRoot,
|
|
4247
|
+
dryRun: options.dryRun === true,
|
|
4248
|
+
listRuns: () => specQueries.getAllWorkflowRuns(1000).flatMap((run) => {
|
|
4249
|
+
if (!run.isolationEnvId)
|
|
4250
|
+
return [];
|
|
4251
|
+
const env = specQueries.getIsolationEnvById(run.isolationEnvId);
|
|
4252
|
+
if (!env)
|
|
4253
|
+
return [];
|
|
4254
|
+
return [{ id: run.id, status: run.status, branch: env.branchName, workingPath: env.workingPath }];
|
|
4255
|
+
}),
|
|
4256
|
+
purge: (runId) => {
|
|
4257
|
+
executor.purge(runId);
|
|
4258
|
+
},
|
|
4259
|
+
});
|
|
4260
|
+
if (options.json) {
|
|
4261
|
+
// eslint-disable-next-line no-console
|
|
4262
|
+
console.log(JSON.stringify(result, null, 2));
|
|
4263
|
+
}
|
|
4264
|
+
else if (!result.defaultBranch) {
|
|
4265
|
+
// eslint-disable-next-line no-console
|
|
4266
|
+
console.log('Default branch unknown (no origin HEAD, main or master) — nothing collected.');
|
|
4267
|
+
}
|
|
4268
|
+
else {
|
|
4269
|
+
const verb = result.dryRun ? 'would remove' : 'removed';
|
|
4270
|
+
for (const c of result.candidates) {
|
|
4271
|
+
// eslint-disable-next-line no-console
|
|
4272
|
+
console.log(` ${verb} ${c.runId} ${c.branch} (${c.workingPath})`);
|
|
4273
|
+
}
|
|
4274
|
+
for (const s of result.skipped) {
|
|
4275
|
+
// eslint-disable-next-line no-console
|
|
4276
|
+
console.log(` kept ${s.runId} — ${s.reason}`);
|
|
4277
|
+
}
|
|
4278
|
+
// eslint-disable-next-line no-console
|
|
4279
|
+
console.log(result.candidates.length === 0
|
|
4280
|
+
? `No merged-run worktrees to collect (default branch: ${result.defaultBranch}).`
|
|
4281
|
+
: result.dryRun
|
|
4282
|
+
? `${result.candidates.length} worktree(s) would be removed. Run without --dry-run to collect.`
|
|
4283
|
+
: `${result.purged.length} worktree(s) removed. Run records and artifacts are kept.`);
|
|
4284
|
+
}
|
|
4285
|
+
break;
|
|
4286
|
+
}
|
|
3475
4287
|
case 'runs': {
|
|
3476
4288
|
const runs = specQueries.getAllWorkflowRuns(50);
|
|
3477
4289
|
if (options.json) {
|
|
@@ -3494,7 +4306,7 @@ function main() {
|
|
|
3494
4306
|
break;
|
|
3495
4307
|
}
|
|
3496
4308
|
default:
|
|
3497
|
-
error(`Unknown workflow action "${action}". Use: list | run | resume | cancel | approve | reject | purge | runs`);
|
|
4309
|
+
error(`Unknown workflow action "${action}". Use: list | run | run-batch | merge-queue | gc | resume | cancel | approve | reject | purge | runs`);
|
|
3498
4310
|
process.exit(1);
|
|
3499
4311
|
}
|
|
3500
4312
|
}
|
|
@@ -3895,6 +4707,67 @@ function main() {
|
|
|
3895
4707
|
cg.close();
|
|
3896
4708
|
}
|
|
3897
4709
|
});
|
|
4710
|
+
/**
|
|
4711
|
+
* specship jira publish --pending — the explicit, one-batch publish of every
|
|
4712
|
+
* requirement sync held back (REQ-THRU-006). Sync only publishes when
|
|
4713
|
+
* `jira.autoPublish: true` is set in specship.config.json; otherwise it
|
|
4714
|
+
* reports the pending count and points here.
|
|
4715
|
+
*/
|
|
4716
|
+
jira
|
|
4717
|
+
.command('publish')
|
|
4718
|
+
.description('Publish specs to the bound JIRA project in one explicit batch (--pending: every requirement sync held back).')
|
|
4719
|
+
.option('--pending', 'publish every requirement with no JIRA issue yet or whose content changed')
|
|
4720
|
+
.option('--dry-run', 'list what would be published without writing to JIRA')
|
|
4721
|
+
.option('--path <projectRoot>', 'project root (default: cwd)')
|
|
4722
|
+
.action(async (options) => {
|
|
4723
|
+
const projectRoot = path.resolve(options.path ?? process.cwd());
|
|
4724
|
+
if (!options.pending) {
|
|
4725
|
+
error('Nothing to publish: pass --pending to publish every requirement sync held back.');
|
|
4726
|
+
process.exit(1);
|
|
4727
|
+
}
|
|
4728
|
+
if (!(0, directory_1.isInitialized)(projectRoot)) {
|
|
4729
|
+
error(`SpecShip not initialized in ${projectRoot}. Run \`specship init -i\` first.`);
|
|
4730
|
+
process.exit(1);
|
|
4731
|
+
}
|
|
4732
|
+
const { SpecShip } = await loadSpecShip();
|
|
4733
|
+
const { publishPendingSpecs, runAutoPublish, summarizeAutoPublishReport, countAutoPublishReport } = await Promise.resolve().then(() => __importStar(require('../jira/auto-publish')));
|
|
4734
|
+
const cg = await SpecShip.open(projectRoot);
|
|
4735
|
+
try {
|
|
4736
|
+
const deps = { specQueries: cg.getSpecQueries(), projectRoot };
|
|
4737
|
+
const report = options.dryRun
|
|
4738
|
+
? await runAutoPublish(deps, { explicit: true, dryRun: true })
|
|
4739
|
+
: await publishPendingSpecs(deps);
|
|
4740
|
+
if (report.skipped === 'unbound') {
|
|
4741
|
+
error('No JIRA binding in specship.config.json — run `specship jira bind` first.');
|
|
4742
|
+
process.exit(1);
|
|
4743
|
+
}
|
|
4744
|
+
if (options.dryRun) {
|
|
4745
|
+
const pending = report.results.filter((r) => r.status === 'pending');
|
|
4746
|
+
// eslint-disable-next-line no-console
|
|
4747
|
+
console.log(`${pending.length} spec(s) would be published:`);
|
|
4748
|
+
// eslint-disable-next-line no-console
|
|
4749
|
+
for (const r of pending)
|
|
4750
|
+
console.log(` ${r.specId}${r.jiraKey ? ` (${r.jiraKey})` : ''}`);
|
|
4751
|
+
return;
|
|
4752
|
+
}
|
|
4753
|
+
const line = summarizeAutoPublishReport(report);
|
|
4754
|
+
// eslint-disable-next-line no-console
|
|
4755
|
+
if (line)
|
|
4756
|
+
console.log(line.replace(/^JIRA auto-publish/, 'JIRA publish'));
|
|
4757
|
+
for (const r of report.results) {
|
|
4758
|
+
if (r.status === 'failed') {
|
|
4759
|
+
// eslint-disable-next-line no-console
|
|
4760
|
+
console.log(`JIRA: ${r.specId} failed — ${r.note ?? 'unknown error'}`);
|
|
4761
|
+
}
|
|
4762
|
+
}
|
|
4763
|
+
if (report.skipped === 'no-credentials' || countAutoPublishReport(report).failed > 0) {
|
|
4764
|
+
process.exitCode = 1;
|
|
4765
|
+
}
|
|
4766
|
+
}
|
|
4767
|
+
finally {
|
|
4768
|
+
cg.close();
|
|
4769
|
+
}
|
|
4770
|
+
});
|
|
3898
4771
|
/**
|
|
3899
4772
|
* specship jira reconcile — detect JIRA-side edits and (with --apply) fold
|
|
3900
4773
|
* them back into the spec (REQ-JIRATEAM-005). Preview is read-only and the
|