@specship/specship-win32-arm64 0.25.0 → 0.29.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 +8 -10
- package/lib/dist/activation/steering.d.ts.map +1 -1
- package/lib/dist/activation/steering.js +21 -6
- 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 +799 -81
- package/lib/dist/bin/specship.js.map +1 -1
- package/lib/dist/commands/specship/learn.md +22 -0
- package/lib/dist/commands/specship/spec.md +19 -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/design-md/check.d.ts +51 -0
- package/lib/dist/design-md/check.d.ts.map +1 -0
- package/lib/dist/design-md/check.js +335 -0
- package/lib/dist/design-md/check.js.map +1 -0
- package/lib/dist/design-md/css.d.ts +40 -0
- package/lib/dist/design-md/css.d.ts.map +1 -0
- package/lib/dist/design-md/css.js +224 -0
- package/lib/dist/design-md/css.js.map +1 -0
- package/lib/dist/design-md/extract.d.ts +84 -0
- package/lib/dist/design-md/extract.d.ts.map +1 -0
- package/lib/dist/design-md/extract.js +595 -0
- package/lib/dist/design-md/extract.js.map +1 -0
- package/lib/dist/design-md/maintain.d.ts +45 -0
- package/lib/dist/design-md/maintain.d.ts.map +1 -0
- package/lib/dist/design-md/maintain.js +127 -0
- package/lib/dist/design-md/maintain.js.map +1 -0
- package/lib/dist/design-md/render.d.ts +28 -0
- package/lib/dist/design-md/render.d.ts.map +1 -0
- package/lib/dist/design-md/render.js +178 -0
- package/lib/dist/design-md/render.js.map +1 -0
- package/lib/dist/design-md/upkeep.d.ts +48 -0
- package/lib/dist/design-md/upkeep.d.ts.map +1 -0
- package/lib/dist/design-md/upkeep.js +169 -0
- package/lib/dist/design-md/upkeep.js.map +1 -0
- package/lib/dist/design-md/verify.d.ts +65 -0
- package/lib/dist/design-md/verify.d.ts.map +1 -0
- package/lib/dist/design-md/verify.js +311 -0
- package/lib/dist/design-md/verify.js.map +1 -0
- package/lib/dist/enforce/changed-files.d.ts +11 -0
- package/lib/dist/enforce/changed-files.d.ts.map +1 -1
- package/lib/dist/enforce/changed-files.js +42 -0
- package/lib/dist/enforce/changed-files.js.map +1 -1
- package/lib/dist/enforce/enforce.d.ts +25 -1
- package/lib/dist/enforce/enforce.d.ts.map +1 -1
- package/lib/dist/enforce/enforce.js +58 -3
- 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/specs/markdown-spec-extractor.d.ts.map +1 -1
- package/lib/dist/extraction/specs/markdown-spec-extractor.js +16 -2
- package/lib/dist/extraction/specs/markdown-spec-extractor.js.map +1 -1
- package/lib/dist/graph/spec-coverage.d.ts +57 -1
- package/lib/dist/graph/spec-coverage.d.ts.map +1 -1
- package/lib/dist/graph/spec-coverage.js +129 -5
- package/lib/dist/graph/spec-coverage.js.map +1 -1
- 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 +8 -3
- package/lib/dist/index.d.ts.map +1 -1
- package/lib/dist/index.js +65 -9
- 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 +271 -0
- package/lib/dist/installer/gemini-commands.js.map +1 -0
- package/lib/dist/installer/index.d.ts +23 -1
- package/lib/dist/installer/index.d.ts.map +1 -1
- package/lib/dist/installer/index.js +22 -3
- package/lib/dist/installer/index.js.map +1 -1
- package/lib/dist/installer/install-defaults.d.ts +19 -0
- package/lib/dist/installer/install-defaults.d.ts.map +1 -1
- package/lib/dist/installer/install-defaults.js +28 -0
- package/lib/dist/installer/install-defaults.js.map +1 -1
- 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 +5 -1
- package/lib/dist/installer/targets/claude.d.ts.map +1 -1
- package/lib/dist/installer/targets/claude.js +15 -27
- 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 +18 -0
- 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/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/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/model-context.d.ts +22 -9
- package/lib/dist/mcp/model-context.d.ts.map +1 -1
- package/lib/dist/mcp/model-context.js +35 -13
- package/lib/dist/mcp/model-context.js.map +1 -1
- package/lib/dist/mcp/proxy.js +1 -1
- 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 +67 -1
- 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 +69 -21
- package/lib/dist/mcp/spec-tools.js.map +1 -1
- package/lib/dist/mcp/tools.d.ts +37 -1
- package/lib/dist/mcp/tools.d.ts.map +1 -1
- package/lib/dist/mcp/tools.js +120 -9
- 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/test-title-links.d.ts.map +1 -1
- package/lib/dist/resolution/test-title-links.js +3 -14
- package/lib/dist/resolution/test-title-links.js.map +1 -1
- package/lib/dist/server/routes/claude.js +15 -7
- package/lib/dist/server/routes/knowledge.js +118 -0
- package/lib/dist/server/server.js +2 -0
- package/lib/dist/skills/design-md/SKILL.md +172 -0
- package/lib/dist/skills/design-md/references/authored-sections.md +90 -0
- package/lib/dist/skills/design-md/references/sources.md +71 -0
- package/lib/dist/spec-lint/lint.d.ts +1 -1
- package/lib/dist/spec-lint/lint.d.ts.map +1 -1
- package/lib/dist/spec-lint/lint.js +63 -2
- package/lib/dist/spec-lint/lint.js.map +1 -1
- 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 +91 -0
- package/lib/dist/spec-quality/postcheck.d.ts.map +1 -0
- package/lib/dist/spec-quality/postcheck.js +421 -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/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 +37 -9
- package/lib/dist/workflows/defaults/spec-author.yaml +11 -9
- package/lib/dist/workflows/defaults/spec-implement-mixed.yaml +21 -436
- package/lib/dist/workflows/defaults/spec-implement.yaml +481 -46
- 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 +2 -2
- package/package.json +5 -3
- 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
|
}
|
|
@@ -941,6 +1001,7 @@ function main() {
|
|
|
941
1001
|
await pushJiraDriftComments(cg, result);
|
|
942
1002
|
await pushJiraAutoPublish(result);
|
|
943
1003
|
pushDriftSummary(cg);
|
|
1004
|
+
await runAutoSweep(cg);
|
|
944
1005
|
clack.outro('Done');
|
|
945
1006
|
cg.destroy();
|
|
946
1007
|
}
|
|
@@ -1358,15 +1419,30 @@ function main() {
|
|
|
1358
1419
|
* Run a reflection pass over the ingested Claude Code transcripts and print the
|
|
1359
1420
|
* self-improvement proposals it surfaces (REQ-REFLECT-006.A1). Headless — no
|
|
1360
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).
|
|
1361
1428
|
*/
|
|
1362
1429
|
program
|
|
1363
|
-
.command('reflect [path]')
|
|
1364
|
-
.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>')
|
|
1365
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)')
|
|
1366
1435
|
.option('--capture', 'Capture a routine as a skill proposal: --title required, content on stdin (LEARN-DOC)')
|
|
1367
1436
|
.option('--title <title>', 'Title for --capture')
|
|
1368
|
-
.
|
|
1369
|
-
|
|
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));
|
|
1370
1446
|
try {
|
|
1371
1447
|
if (!(0, directory_1.isInitialized)(projectPath)) {
|
|
1372
1448
|
error(`SpecShip not initialized in ${projectPath}`);
|
|
@@ -1374,6 +1450,17 @@ function main() {
|
|
|
1374
1450
|
}
|
|
1375
1451
|
const { default: SpecShip } = await loadSpecShip();
|
|
1376
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
|
+
}
|
|
1377
1464
|
// Explicit capture door (LEARN-DOC, REQ-LEARN-002): crystallize a
|
|
1378
1465
|
// distilled routine into a human-gated skill proposal. Content on
|
|
1379
1466
|
// stdin so multi-line routines survive shell quoting.
|
|
@@ -1390,13 +1477,18 @@ function main() {
|
|
|
1390
1477
|
error('--capture expects the routine content on stdin (pipe or heredoc)');
|
|
1391
1478
|
process.exit(1);
|
|
1392
1479
|
}
|
|
1393
|
-
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 });
|
|
1394
1486
|
if (options.json) {
|
|
1395
1487
|
console.log(JSON.stringify(p, null, 2));
|
|
1396
1488
|
}
|
|
1397
1489
|
else {
|
|
1398
1490
|
info(`Captured as proposal ${p.contentHash.slice(0, 12)} (${p.state}): ${p.title}`);
|
|
1399
|
-
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.`);
|
|
1400
1492
|
}
|
|
1401
1493
|
cg.destroy();
|
|
1402
1494
|
return;
|
|
@@ -1407,7 +1499,9 @@ function main() {
|
|
|
1407
1499
|
cg.destroy();
|
|
1408
1500
|
return;
|
|
1409
1501
|
}
|
|
1410
|
-
|
|
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) {
|
|
1411
1505
|
info('No proposals yet — not enough signal in the ingested transcripts.');
|
|
1412
1506
|
info('Tip: run the dashboard with `specship desktop --ingest` to build transcript history.');
|
|
1413
1507
|
cg.destroy();
|
|
@@ -1426,13 +1520,14 @@ function main() {
|
|
|
1426
1520
|
console.log(chalk.bold(`\nReflection proposals (${result.open.length}):\n`));
|
|
1427
1521
|
for (const p of result.open) {
|
|
1428
1522
|
const sev = (sevColor[p.severity] ?? chalk.white)(p.severity.toUpperCase().padEnd(5));
|
|
1429
|
-
console.log(`${sev} ${chalk.bold(p.title)}`);
|
|
1523
|
+
console.log(`${sev} ${chalk.bold(p.title)} ${chalk.cyan(p.contentHash.slice(0, DISPLAY_HASH_LEN))}`);
|
|
1430
1524
|
console.log(chalk.dim(` ${typeLabel[p.type] ?? p.type} → ${p.targetPath}`));
|
|
1431
1525
|
console.log(chalk.dim(` ${p.evidence.detail}`));
|
|
1432
1526
|
console.log(chalk.dim(` ${p.body}`));
|
|
1433
1527
|
console.log();
|
|
1434
1528
|
}
|
|
1435
1529
|
info('These are proposals only — nothing is written until you apply one.');
|
|
1530
|
+
info(REFLECT_REVIEW_HINT);
|
|
1436
1531
|
cg.destroy();
|
|
1437
1532
|
}
|
|
1438
1533
|
catch (err) {
|
|
@@ -1746,6 +1841,7 @@ function main() {
|
|
|
1746
1841
|
.option('-p, --path <path>', 'Project path')
|
|
1747
1842
|
.option('-j, --json', 'Output as JSON')
|
|
1748
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')
|
|
1749
1845
|
.action(async (specArg, options) => {
|
|
1750
1846
|
const projectPath = resolveProjectPath(options.path);
|
|
1751
1847
|
try {
|
|
@@ -1753,17 +1849,24 @@ function main() {
|
|
|
1753
1849
|
error(`SpecShip not initialized in ${projectPath}`);
|
|
1754
1850
|
process.exit(1);
|
|
1755
1851
|
}
|
|
1756
|
-
const { default: SpecShip, formatCoverageLine } = await loadSpecShip();
|
|
1852
|
+
const { default: SpecShip, formatCoverageLine, requireKindsExitCode, criteriaMissingKind } = await loadSpecShip();
|
|
1757
1853
|
const cg = await SpecShip.open(projectPath);
|
|
1758
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);
|
|
1759
1858
|
if (options.json) {
|
|
1760
1859
|
console.log(JSON.stringify(report, null, 2));
|
|
1761
1860
|
cg.destroy();
|
|
1861
|
+
if (kindsExit !== 0)
|
|
1862
|
+
process.exit(kindsExit);
|
|
1762
1863
|
return;
|
|
1763
1864
|
}
|
|
1764
1865
|
if (options.line) {
|
|
1765
1866
|
console.log(formatCoverageLine(report));
|
|
1766
1867
|
cg.destroy();
|
|
1868
|
+
if (kindsExit !== 0)
|
|
1869
|
+
process.exit(kindsExit);
|
|
1767
1870
|
return;
|
|
1768
1871
|
}
|
|
1769
1872
|
if (report.requirements.length === 0) {
|
|
@@ -1779,15 +1882,29 @@ function main() {
|
|
|
1779
1882
|
untested: chalk.dim('·'),
|
|
1780
1883
|
broken: chalk.red('✗'),
|
|
1781
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
|
+
};
|
|
1782
1892
|
for (const req of report.requirements) {
|
|
1783
|
-
console.log(`${mark[req.verdict]} ${chalk.bold(req.specId)} ${chalk.dim(req.verdict)} — ${req.title}`
|
|
1893
|
+
console.log(`${mark[req.verdict]} ${chalk.bold(req.specId)} ${chalk.dim(req.verdict)} — ${req.title}` +
|
|
1894
|
+
(req.criteria.length === 0 ? ` ${kindNote(req)}` : ''));
|
|
1784
1895
|
for (const c of req.criteria) {
|
|
1785
|
-
console.log(` ${mark[c.verdict]} ${c.specId} ${chalk.dim(`${c.verdict} · implements:${c.implementsLinks.count} tests:${c.testsLinks.count}`)}`);
|
|
1896
|
+
console.log(` ${mark[c.verdict]} ${c.specId} ${chalk.dim(`${c.verdict} · implements:${c.implementsLinks.count} tests:${c.testsLinks.count}`)} · ${kindNote(c)}`);
|
|
1786
1897
|
}
|
|
1787
1898
|
}
|
|
1788
1899
|
console.log();
|
|
1789
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
|
+
}
|
|
1790
1905
|
cg.destroy();
|
|
1906
|
+
if (kindsExit !== 0)
|
|
1907
|
+
process.exit(kindsExit);
|
|
1791
1908
|
}
|
|
1792
1909
|
catch (err) {
|
|
1793
1910
|
error(`coverage failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -1822,6 +1939,184 @@ function main() {
|
|
|
1822
1939
|
process.exit(1);
|
|
1823
1940
|
}
|
|
1824
1941
|
});
|
|
1942
|
+
/**
|
|
1943
|
+
* design.md (DESIGNMD-DOC). `extract` turns a Claude Design snapshot or a
|
|
1944
|
+
* Figma export into a pixel-exact reference; `check` fails one that is
|
|
1945
|
+
* incomplete, stale or vague. File-based, needs no index.
|
|
1946
|
+
*/
|
|
1947
|
+
program
|
|
1948
|
+
.command('design <action> [paths...]')
|
|
1949
|
+
.description('design.md reference: extract <source...> [--out|--update <design.md>] | check <design.md> | verify <build.html> --against <design.md>')
|
|
1950
|
+
.option('-p, --path <path>', 'Project path (paths are resolved against it; defaults to cwd)')
|
|
1951
|
+
.option('-o, --out <file>', 'extract: write a new design.md')
|
|
1952
|
+
.option('-u, --update <file>', 'extract: refresh the generated block of an existing design.md')
|
|
1953
|
+
.option('-t, --title <title>', 'extract --out: the design title')
|
|
1954
|
+
.option('--origin <link>', 'extract --out: where the design came from (claude.ai/design or figma.com link)')
|
|
1955
|
+
.option('-j, --json', 'check: output as JSON')
|
|
1956
|
+
.option('-a, --against <file>', 'verify: the design.md to compare the build with')
|
|
1957
|
+
.action(async (action, paths, options) => {
|
|
1958
|
+
const projectRoot = resolveProjectPath(options.path);
|
|
1959
|
+
const { runDesignExtractCli, runDesignCheckCli, runDesignVerifyCli } = await Promise.resolve().then(() => __importStar(require('../design-md/check')));
|
|
1960
|
+
const flags = (pairs) => pairs.flatMap(([k, v]) => (v ? [k, v] : []));
|
|
1961
|
+
let result;
|
|
1962
|
+
if (action === 'extract') {
|
|
1963
|
+
result = runDesignExtractCli([...(paths ?? []), ...flags([['--out', options.out], ['--update', options.update], ['--title', options.title], ['--origin', options.origin]])], { projectRoot });
|
|
1964
|
+
}
|
|
1965
|
+
else if (action === 'check') {
|
|
1966
|
+
result = runDesignCheckCli([...(paths ?? []), ...(options.json ? ['--json'] : [])], { projectRoot });
|
|
1967
|
+
}
|
|
1968
|
+
else if (action === 'verify') {
|
|
1969
|
+
result = runDesignVerifyCli([...(paths ?? []), ...flags([['--against', options.against]])], { projectRoot });
|
|
1970
|
+
}
|
|
1971
|
+
else {
|
|
1972
|
+
error(`unknown design action "${action}" — use extract, check or verify`);
|
|
1973
|
+
process.exit(1);
|
|
1974
|
+
}
|
|
1975
|
+
console.log(result.output);
|
|
1976
|
+
if (result.exitCode !== 0)
|
|
1977
|
+
process.exit(result.exitCode);
|
|
1978
|
+
});
|
|
1979
|
+
/**
|
|
1980
|
+
* specship knowledge <action> — the knowledge layer (KNOWLEDGE-DOC).
|
|
1981
|
+
* stale external facts whose dependency version changed (REQ-KNOW-002/006)
|
|
1982
|
+
* check enforced rules → violations (REQ-KNOW-003)
|
|
1983
|
+
* proposals pending proposals drafted by the implement workflow (REQ-KNOW-004)
|
|
1984
|
+
* apply <id> move a confirmed proposal into specs/domain/ (human-only)
|
|
1985
|
+
* reject <id> delete a proposal
|
|
1986
|
+
* File-based: needs no index, so it runs in CI and inside run worktrees.
|
|
1987
|
+
*/
|
|
1988
|
+
program
|
|
1989
|
+
.command('knowledge <action> [id]')
|
|
1990
|
+
.description('Knowledge layer: stale | check | proposals | apply <id> | reject <id>')
|
|
1991
|
+
.option('-p, --path <path>', 'Project path (defaults to cwd)')
|
|
1992
|
+
.option('-j, --json', 'Output as JSON')
|
|
1993
|
+
.action(async (action, id, options) => {
|
|
1994
|
+
const projectRoot = options.path ? path.resolve(options.path) : process.cwd();
|
|
1995
|
+
const out = (value, textOut) => console.log(options.json ? JSON.stringify(value, null, 2) : textOut);
|
|
1996
|
+
try {
|
|
1997
|
+
if (action === 'stale') {
|
|
1998
|
+
const { findStaleExternalFacts, formatStaleReport, exitCodeForStale } = await Promise.resolve().then(() => __importStar(require('../knowledge/stale')));
|
|
1999
|
+
const result = findStaleExternalFacts(projectRoot);
|
|
2000
|
+
out(result, formatStaleReport(result));
|
|
2001
|
+
process.exit(exitCodeForStale(result));
|
|
2002
|
+
}
|
|
2003
|
+
else if (action === 'check') {
|
|
2004
|
+
const { checkEnforcedRules, formatEnforceReport, exitCodeForEnforce } = await Promise.resolve().then(() => __importStar(require('../knowledge/enforce')));
|
|
2005
|
+
const result = checkEnforcedRules(projectRoot);
|
|
2006
|
+
out(result, formatEnforceReport(result));
|
|
2007
|
+
process.exit(exitCodeForEnforce(result));
|
|
2008
|
+
}
|
|
2009
|
+
else if (action === 'proposals') {
|
|
2010
|
+
const { listProposals, formatProposalList } = await Promise.resolve().then(() => __importStar(require('../knowledge/proposals')));
|
|
2011
|
+
const list = listProposals(projectRoot);
|
|
2012
|
+
out(list, formatProposalList(list));
|
|
2013
|
+
}
|
|
2014
|
+
else if (action === 'apply' || action === 'reject') {
|
|
2015
|
+
if (!id) {
|
|
2016
|
+
error(`knowledge ${action} needs a proposal id — see \`specship knowledge proposals\``);
|
|
2017
|
+
process.exit(1);
|
|
2018
|
+
}
|
|
2019
|
+
const { applyProposal, rejectProposal, ProposalRefusedError } = await Promise.resolve().then(() => __importStar(require('../knowledge/proposals')));
|
|
2020
|
+
if (action === 'reject') {
|
|
2021
|
+
rejectProposal(projectRoot, id);
|
|
2022
|
+
out({ rejected: id }, `Rejected proposal ${id} — nothing was written to specs/domain/.`);
|
|
2023
|
+
}
|
|
2024
|
+
else {
|
|
2025
|
+
try {
|
|
2026
|
+
const applied = applyProposal(projectRoot, id);
|
|
2027
|
+
out(applied, `Applied ${id} as ${applied.id} → ${applied.path}. Run \`specship sync\` to index it.`);
|
|
2028
|
+
}
|
|
2029
|
+
catch (err) {
|
|
2030
|
+
if (err instanceof ProposalRefusedError) {
|
|
2031
|
+
error(`Refused to apply ${id} — fix the proposal first:`);
|
|
2032
|
+
for (const f of err.findings)
|
|
2033
|
+
console.log(` ${f.severity} ${f.code}: ${f.message}`);
|
|
2034
|
+
process.exit(1);
|
|
2035
|
+
}
|
|
2036
|
+
throw err;
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
else {
|
|
2041
|
+
error(`unknown knowledge action "${action}" — expected stale | check | proposals | apply | reject`);
|
|
2042
|
+
process.exit(1);
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
catch (err) {
|
|
2046
|
+
error(`knowledge ${action} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2047
|
+
process.exit(1);
|
|
2048
|
+
}
|
|
2049
|
+
});
|
|
2050
|
+
/**
|
|
2051
|
+
* specship plan-scope <SPEC_ID> --plan-file <f> — small-plan auto-approval
|
|
2052
|
+
* check (THROUGHPUT-DOC, REQ-THRU-003). Always exits 0; the AUTO_APPROVE
|
|
2053
|
+
* marker is what the plan gate reads.
|
|
2054
|
+
*/
|
|
2055
|
+
program
|
|
2056
|
+
.command('plan-scope <specId>')
|
|
2057
|
+
.description('Decide whether a plan may skip the plan gate (opt-in; prints AUTO_APPROVE=yes|no)')
|
|
2058
|
+
.requiredOption('--plan-file <file>', 'File holding the plan text')
|
|
2059
|
+
.option('-p, --path <path>', 'Project path (defaults to cwd)')
|
|
2060
|
+
.action(async (specId, options) => {
|
|
2061
|
+
const projectRoot = options.path ? path.resolve(options.path) : process.cwd();
|
|
2062
|
+
try {
|
|
2063
|
+
const { assessPlanScopeFromFile, formatPlanScope } = await Promise.resolve().then(() => __importStar(require('../spec-quality/plan-scope')));
|
|
2064
|
+
const planText = fs.readFileSync(path.resolve(options.planFile), 'utf8');
|
|
2065
|
+
console.log(formatPlanScope(assessPlanScopeFromFile(projectRoot, specId, planText)));
|
|
2066
|
+
}
|
|
2067
|
+
catch (err) {
|
|
2068
|
+
console.log(`AUTO_APPROVE=no\n- plan-scope failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2069
|
+
}
|
|
2070
|
+
});
|
|
2071
|
+
/**
|
|
2072
|
+
* specship ready <SPEC_ID> — readiness gate (PIPETRUST-DOC, REQ-PIPETRUST-004).
|
|
2073
|
+
* File-based (no index needed), so it runs inside a workflow worktree. Exits
|
|
2074
|
+
* non-zero when the requirement is not ready; prints the evidence plan.
|
|
2075
|
+
*/
|
|
2076
|
+
program
|
|
2077
|
+
.command('ready <specId>')
|
|
2078
|
+
.description('Check a requirement is ready to implement and print its evidence plan (exits non-zero when not ready)')
|
|
2079
|
+
.option('-p, --path <path>', 'Project path (defaults to cwd)')
|
|
2080
|
+
.option('-j, --json', 'Output as JSON')
|
|
2081
|
+
.action(async (specId, options) => {
|
|
2082
|
+
const projectRoot = options.path ? path.resolve(options.path) : process.cwd();
|
|
2083
|
+
try {
|
|
2084
|
+
const { assessReadinessFromFile, formatEvidencePlan } = await Promise.resolve().then(() => __importStar(require('../spec-quality/readiness')));
|
|
2085
|
+
const result = assessReadinessFromFile(projectRoot, specId);
|
|
2086
|
+
console.log(options.json ? JSON.stringify(result, null, 2) : formatEvidencePlan(result));
|
|
2087
|
+
if (!result.ready)
|
|
2088
|
+
process.exit(1);
|
|
2089
|
+
}
|
|
2090
|
+
catch (err) {
|
|
2091
|
+
error(`ready failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2092
|
+
process.exit(1);
|
|
2093
|
+
}
|
|
2094
|
+
});
|
|
2095
|
+
/**
|
|
2096
|
+
* specship postcheck <SPEC_ID> [--fix] — mechanical post-implement checks
|
|
2097
|
+
* (PIPETRUST-DOC, REQ-PIPETRUST-005). File-based, worktree-safe.
|
|
2098
|
+
*/
|
|
2099
|
+
program
|
|
2100
|
+
.command('postcheck <specId>')
|
|
2101
|
+
.description('Mechanically check an implemented requirement: verifies coverage, CHANGELOG, configured checks (exits non-zero on failure)')
|
|
2102
|
+
.option('-p, --path <path>', 'Project path (defaults to cwd)')
|
|
2103
|
+
.option('--fix', 'Run each configured check\'s fix command first')
|
|
2104
|
+
.option('--base <ref>', 'Base ref for the CHANGELOG check (defaults to the recorded run base, then the merge-base with the default branch)')
|
|
2105
|
+
.option('-j, --json', 'Output as JSON')
|
|
2106
|
+
.action(async (specId, options) => {
|
|
2107
|
+
const projectRoot = options.path ? path.resolve(options.path) : process.cwd();
|
|
2108
|
+
try {
|
|
2109
|
+
const { runPostcheck, formatPostcheck } = await Promise.resolve().then(() => __importStar(require('../spec-quality/postcheck')));
|
|
2110
|
+
const result = runPostcheck({ projectRoot, requirementId: specId, fix: options.fix, base: options.base });
|
|
2111
|
+
console.log(options.json ? JSON.stringify(result, null, 2) : formatPostcheck(result));
|
|
2112
|
+
if (!result.ok)
|
|
2113
|
+
process.exit(1);
|
|
2114
|
+
}
|
|
2115
|
+
catch (err) {
|
|
2116
|
+
error(`postcheck failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2117
|
+
process.exit(1);
|
|
2118
|
+
}
|
|
2119
|
+
});
|
|
1825
2120
|
/**
|
|
1826
2121
|
* specship verify --report <file>
|
|
1827
2122
|
*
|
|
@@ -2021,9 +2316,12 @@ function main() {
|
|
|
2021
2316
|
}
|
|
2022
2317
|
for (const c of r.checks) {
|
|
2023
2318
|
const tag = c.gating ? chalk.dim('[gating]') : chalk.dim('[advisory]');
|
|
2024
|
-
|
|
2319
|
+
// A skipped check's input was unavailable — presented as skipped,
|
|
2320
|
+
// never as pass or fail (REQ-QGATE-002.A3).
|
|
2321
|
+
const mark = c.skipped ? chalk.yellow('○') : c.passed ? chalk.green('✓') : (c.gating ? chalk.red('✗') : chalk.yellow('•'));
|
|
2025
2322
|
const reach = scope ? chalk.dim(c.scoped ? ' [since]' : ' [repo-wide]') : '';
|
|
2026
|
-
|
|
2323
|
+
const status = c.skipped ? 'skipped' : c.passed ? 'pass' : `${c.findings.length} finding(s)`;
|
|
2324
|
+
console.log(` ${mark} ${c.check.padEnd(16)} ${tag}${reach} ${status}`);
|
|
2027
2325
|
if (!c.passed)
|
|
2028
2326
|
for (const f of c.findings.slice(0, 8))
|
|
2029
2327
|
console.log(chalk.dim(` ${f}`));
|
|
@@ -2037,7 +2335,7 @@ function main() {
|
|
|
2037
2335
|
: 'Pass (no gating checks failed).');
|
|
2038
2336
|
// The advisory sell (REQ-ENFORCE-004.A1): findings that would fail
|
|
2039
2337
|
// a gated run end the report with the exact opt-in command.
|
|
2040
|
-
const wouldGate = r.checks.filter((c) => !c.gating && !c.passed).map((c) => c.check);
|
|
2338
|
+
const wouldGate = r.checks.filter((c) => !c.gating && !c.passed && !c.skipped).map((c) => c.check);
|
|
2041
2339
|
if (wouldGate.length) {
|
|
2042
2340
|
const total = r.checks
|
|
2043
2341
|
.filter((c) => wouldGate.includes(c.check))
|
|
@@ -2162,6 +2460,39 @@ function main() {
|
|
|
2162
2460
|
process.exit(1);
|
|
2163
2461
|
}
|
|
2164
2462
|
}
|
|
2463
|
+
program
|
|
2464
|
+
.command('suppressions [path]')
|
|
2465
|
+
.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')
|
|
2466
|
+
.option('--since <ref>', 'Diff base (default HEAD): scan lines added since this ref')
|
|
2467
|
+
.option('-j, --json', 'Output findings as JSON')
|
|
2468
|
+
.action(async (pathArg, options) => {
|
|
2469
|
+
const projectPath = resolveProjectPath(pathArg);
|
|
2470
|
+
// The SUPPRESSION_GATE marker is the machine-readable contract the
|
|
2471
|
+
// spec-implement workflows route on (REQ-QGATE-004.A3) — same doctrine as
|
|
2472
|
+
// VERIFY_RESULT: a scan that could not run is `skipped`, never `clean`.
|
|
2473
|
+
try {
|
|
2474
|
+
const { workingTreeDiffText } = await Promise.resolve().then(() => __importStar(require('../enforce/changed-files')));
|
|
2475
|
+
const { scanDisableComments, formatSuppressionFinding } = await Promise.resolve().then(() => __importStar(require('../enforce/suppressions')));
|
|
2476
|
+
const findings = scanDisableComments(workingTreeDiffText(projectPath, options.since ?? 'HEAD'));
|
|
2477
|
+
if (options.json) {
|
|
2478
|
+
console.log(JSON.stringify({ gate: findings.length === 0 ? 'clean' : 'failed', findings }, null, 2));
|
|
2479
|
+
process.exit(findings.length === 0 ? 0 : 1);
|
|
2480
|
+
}
|
|
2481
|
+
if (findings.length === 0) {
|
|
2482
|
+
console.log('SUPPRESSION_GATE=clean');
|
|
2483
|
+
return;
|
|
2484
|
+
}
|
|
2485
|
+
console.log('SUPPRESSION_GATE=failed');
|
|
2486
|
+
for (const f of findings)
|
|
2487
|
+
console.log(` ${formatSuppressionFinding(f)}`);
|
|
2488
|
+
console.log('Fix the code instead of silencing the rule (REQ-QGATE-004).');
|
|
2489
|
+
process.exit(1);
|
|
2490
|
+
}
|
|
2491
|
+
catch (err) {
|
|
2492
|
+
console.log('SUPPRESSION_GATE=skipped');
|
|
2493
|
+
console.log(`SUPPRESSION_REASON=${err instanceof Error ? err.message.split('\n')[0] : String(err)}`);
|
|
2494
|
+
}
|
|
2495
|
+
});
|
|
2165
2496
|
program
|
|
2166
2497
|
.command('serve')
|
|
2167
2498
|
.description('Start SpecShip as an MCP server for AI assistants (stdio transport)')
|
|
@@ -2663,6 +2994,7 @@ function main() {
|
|
|
2663
2994
|
// existing invocations — including our own offline-install scripts — behave
|
|
2664
2995
|
// exactly as before.
|
|
2665
2996
|
.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')
|
|
2997
|
+
.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')
|
|
2666
2998
|
.action(async (opts) => {
|
|
2667
2999
|
if (opts.printConfig) {
|
|
2668
3000
|
const loc = (opts.location === 'local' ? 'local' : 'global');
|
|
@@ -2723,6 +3055,10 @@ function main() {
|
|
|
2723
3055
|
withJira: opts.withJira === true ? true : undefined,
|
|
2724
3056
|
withDesigner: opts.withDesigner === true ? true : undefined,
|
|
2725
3057
|
statusLine,
|
|
3058
|
+
// REQ-AUDFIX-001: a local install must honor --skip-index too.
|
|
3059
|
+
skipIndex: opts.skipIndex === true,
|
|
3060
|
+
// REQ-GEMINI-009: strictly opt-in, no prompt. Claude target ignores it.
|
|
3061
|
+
trust: opts.trust === true ? true : undefined,
|
|
2726
3062
|
yes: opts.yes,
|
|
2727
3063
|
});
|
|
2728
3064
|
// Index the current project (REQ-HANDSHAKE-004) before the smoke check,
|
|
@@ -2899,7 +3235,32 @@ function main() {
|
|
|
2899
3235
|
}
|
|
2900
3236
|
}
|
|
2901
3237
|
const { buildCheatsheetPayload } = await Promise.resolve().then(() => __importStar(require('../activation/cheatsheet')));
|
|
2902
|
-
|
|
3238
|
+
// Learned-state extras (REQ-ALEARN-002 / REQ-ALEARN-004.A2) — both
|
|
3239
|
+
// best-effort: an unreadable file or DB silently yields the base
|
|
3240
|
+
// cheatsheet, never an error in the SessionStart hook.
|
|
3241
|
+
const projectPath = resolveProjectPath(undefined);
|
|
3242
|
+
let conventions = null;
|
|
3243
|
+
try {
|
|
3244
|
+
conventions = (await Promise.resolve().then(() => __importStar(require('fs')))).readFileSync((await Promise.resolve().then(() => __importStar(require('path')))).join(projectPath, '.specship', 'conventions.md'), 'utf-8');
|
|
3245
|
+
}
|
|
3246
|
+
catch { /* no conventions file */ }
|
|
3247
|
+
let openProposals = 0;
|
|
3248
|
+
try {
|
|
3249
|
+
const { DatabaseConnection, DATABASE_FILENAME } = await Promise.resolve().then(() => __importStar(require('../db')));
|
|
3250
|
+
const { ReflectStore } = await Promise.resolve().then(() => __importStar(require('../reflect')));
|
|
3251
|
+
const dbPath = (await Promise.resolve().then(() => __importStar(require('path')))).join(projectPath, '.specship', DATABASE_FILENAME);
|
|
3252
|
+
if ((await Promise.resolve().then(() => __importStar(require('fs')))).existsSync(dbPath)) {
|
|
3253
|
+
const conn = DatabaseConnection.initialize(dbPath);
|
|
3254
|
+
try {
|
|
3255
|
+
openProposals = new ReflectStore(conn.getDb()).list('open').length;
|
|
3256
|
+
}
|
|
3257
|
+
finally {
|
|
3258
|
+
conn.close();
|
|
3259
|
+
}
|
|
3260
|
+
}
|
|
3261
|
+
}
|
|
3262
|
+
catch { /* store unavailable → omit the line */ }
|
|
3263
|
+
const payload = buildCheatsheetPayload(process.env, { conventions, openProposals });
|
|
2903
3264
|
if (payload)
|
|
2904
3265
|
process.stdout.write(JSON.stringify(payload) + '\n');
|
|
2905
3266
|
process.exit(0);
|
|
@@ -2937,21 +3298,14 @@ function main() {
|
|
|
2937
3298
|
}
|
|
2938
3299
|
catch { /* no stdin — nothing to nudge */ }
|
|
2939
3300
|
const raw = Buffer.concat(chunks).toString('utf-8').trim();
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
prompt = typeof parsed?.prompt === 'string' ? parsed.prompt : raw;
|
|
2945
|
-
}
|
|
2946
|
-
catch {
|
|
2947
|
-
prompt = raw; // not JSON — treat as the raw prompt text
|
|
2948
|
-
}
|
|
2949
|
-
}
|
|
2950
|
-
if (shouldNudge(prompt)) {
|
|
3301
|
+
const { parseHookPayload, buildHookAdditionalContext, deriveHostTraits } = await Promise.resolve().then(() => __importStar(require('../activation/hook-output')));
|
|
3302
|
+
const payload = parseHookPayload(raw);
|
|
3303
|
+
const traits = deriveHostTraits(payload);
|
|
3304
|
+
if (shouldNudge(payload.prompt || raw)) {
|
|
2951
3305
|
const additionalContext = 'This repo uses spec-driven development (SpecShip). Before any brainstorming or ' +
|
|
2952
3306
|
'planning skill, FIRST invoke spec-author to author the spec under specs/ for this ' +
|
|
2953
3307
|
'work — the spec is the contract; implement from it with /specship:spec implement.';
|
|
2954
|
-
process.stdout.write(
|
|
3308
|
+
process.stdout.write(buildHookAdditionalContext(additionalContext, payload.hookEventName, traits.fallbackPromptEvent));
|
|
2955
3309
|
}
|
|
2956
3310
|
process.exit(0);
|
|
2957
3311
|
});
|
|
@@ -2967,11 +3321,9 @@ function main() {
|
|
|
2967
3321
|
.command('steer-nudge')
|
|
2968
3322
|
.description('Internal hook: steer flow/structure questions to specship_explore (UserPromptSubmit)')
|
|
2969
3323
|
.action(async () => {
|
|
2970
|
-
// Prefer the hook payload's cwd (Claude Code
|
|
2971
|
-
// check targets the project the prompt belongs to, not
|
|
2972
|
-
// process happened to spawn.
|
|
2973
|
-
let cwd = process.cwd();
|
|
2974
|
-
let transcriptPath = null;
|
|
3324
|
+
// Prefer the hook payload's cwd (Claude Code / Gemini CLI both send it
|
|
3325
|
+
// on stdin) so the check targets the project the prompt belongs to, not
|
|
3326
|
+
// wherever the hook process happened to spawn.
|
|
2975
3327
|
const chunks = [];
|
|
2976
3328
|
try {
|
|
2977
3329
|
for await (const c of process.stdin)
|
|
@@ -2979,81 +3331,100 @@ function main() {
|
|
|
2979
3331
|
}
|
|
2980
3332
|
catch { /* no stdin — fall back to process.cwd() */ }
|
|
2981
3333
|
const raw = Buffer.concat(chunks).toString('utf-8').trim();
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
cwd = parsed.cwd;
|
|
2987
|
-
if (typeof parsed?.transcript_path === 'string' && parsed.transcript_path.length > 0) {
|
|
2988
|
-
transcriptPath = parsed.transcript_path;
|
|
2989
|
-
}
|
|
2990
|
-
}
|
|
2991
|
-
catch { /* not JSON — keep process.cwd() */ }
|
|
2992
|
-
}
|
|
3334
|
+
const { parseHookPayload, buildHookAdditionalContext, deriveHostTraits } = await Promise.resolve().then(() => __importStar(require('../activation/hook-output')));
|
|
3335
|
+
const payload = parseHookPayload(raw);
|
|
3336
|
+
const traits = deriveHostTraits(payload);
|
|
3337
|
+
const cwd = payload.cwd ?? process.cwd();
|
|
2993
3338
|
// Record the session's model from the transcript tail (MODCTX-DOC,
|
|
2994
3339
|
// REQ-MODCTX-001.A4) — the primary detection channel, present on every
|
|
2995
3340
|
// default install. Fires per prompt so mid-session /model switches
|
|
2996
3341
|
// track; runs even when the steering text is suppressed, but only in
|
|
2997
3342
|
// initialized projects. Best-effort: never fails the hook.
|
|
2998
|
-
if (transcriptPath && fs.existsSync(path.join(cwd, '.specship'))) {
|
|
3343
|
+
if (payload.transcriptPath && fs.existsSync(path.join(cwd, '.specship'))) {
|
|
2999
3344
|
try {
|
|
3000
3345
|
const { readModelFromTranscript, recordSessionModel } = await Promise.resolve().then(() => __importStar(require('../mcp/model-context')));
|
|
3001
|
-
const model = readModelFromTranscript(transcriptPath);
|
|
3346
|
+
const model = readModelFromTranscript(payload.transcriptPath);
|
|
3002
3347
|
if (model)
|
|
3003
3348
|
recordSessionModel(cwd, model);
|
|
3004
3349
|
}
|
|
3005
3350
|
catch { /* telemetry must never break the hook */ }
|
|
3006
3351
|
}
|
|
3007
3352
|
const { buildSteeringNudge } = await Promise.resolve().then(() => __importStar(require('../activation/steering')));
|
|
3008
|
-
const additionalContext = buildSteeringNudge(cwd);
|
|
3353
|
+
const additionalContext = buildSteeringNudge(cwd, process.env, undefined, traits.toolPrefix);
|
|
3009
3354
|
if (additionalContext) {
|
|
3010
|
-
process.stdout.write(
|
|
3355
|
+
process.stdout.write(buildHookAdditionalContext(additionalContext, payload.hookEventName, traits.fallbackPromptEvent));
|
|
3011
3356
|
}
|
|
3012
3357
|
process.exit(0);
|
|
3013
3358
|
});
|
|
3014
3359
|
/**
|
|
3015
|
-
* specship search-intercept (STEER-HOOK-DOC, REQ-STEER-004/005)
|
|
3360
|
+
* specship search-intercept (STEER-HOOK-DOC, REQ-STEER-004/005, REQ-GEMINI-011)
|
|
3016
3361
|
*
|
|
3017
|
-
* Internal
|
|
3018
|
-
*
|
|
3019
|
-
*
|
|
3020
|
-
*
|
|
3021
|
-
*
|
|
3022
|
-
*
|
|
3362
|
+
* Internal hook on search-shaped tools. For Claude's PreToolUse (Grep/Glob)
|
|
3363
|
+
* and Gemini's BeforeTool (grep_search / glob / run_shell_command), emit the
|
|
3364
|
+
* point-of-use redirect to specship_explore. Host-aware traits are derived
|
|
3365
|
+
* from the payload in `activation/hook-output.ts`; shared code never sniffs
|
|
3366
|
+
* event names. Advisory for Claude; Gemini also supports strict mode
|
|
3367
|
+
* (`SPECSHIP_GEMINI_STRICT=1`) which denies grep_search / glob — never
|
|
3368
|
+
* run_shell_command (REQ-GEMINI-011.A3). Exits 0 on every path.
|
|
3023
3369
|
*/
|
|
3024
3370
|
program
|
|
3025
3371
|
.command('search-intercept')
|
|
3026
|
-
.description('Internal hook: point-of-use redirect from search tools to specship_explore (PreToolUse)')
|
|
3372
|
+
.description('Internal hook: point-of-use redirect from search tools to specship_explore (PreToolUse / BeforeTool)')
|
|
3027
3373
|
.action(async () => {
|
|
3028
3374
|
try {
|
|
3029
|
-
let cwd = process.cwd();
|
|
3030
|
-
let sessionId = null;
|
|
3031
|
-
let transcriptPath = null;
|
|
3032
3375
|
const chunks = [];
|
|
3033
3376
|
try {
|
|
3034
3377
|
for await (const c of process.stdin)
|
|
3035
3378
|
chunks.push(c);
|
|
3036
3379
|
}
|
|
3037
|
-
catch { /* no stdin — fall
|
|
3038
|
-
const raw = Buffer.concat(chunks).toString('utf-8')
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3380
|
+
catch { /* no stdin — fall through with empty payload */ }
|
|
3381
|
+
const raw = Buffer.concat(chunks).toString('utf-8');
|
|
3382
|
+
const { parseHookPayload, deriveHostTraits, buildHookAdditionalContext, buildHookDenyDecision } = await Promise.resolve().then(() => __importStar(require('../activation/hook-output')));
|
|
3383
|
+
const payload = parseHookPayload(raw);
|
|
3384
|
+
const traits = deriveHostTraits(payload);
|
|
3385
|
+
const cwd = payload.cwd ?? process.cwd();
|
|
3386
|
+
let toolName = '';
|
|
3387
|
+
let toolInput = {};
|
|
3388
|
+
try {
|
|
3389
|
+
const parsed = JSON.parse(raw);
|
|
3390
|
+
if (typeof parsed?.tool_name === 'string')
|
|
3391
|
+
toolName = parsed.tool_name;
|
|
3392
|
+
if (parsed?.tool_input && typeof parsed.tool_input === 'object' && !Array.isArray(parsed.tool_input)) {
|
|
3393
|
+
toolInput = parsed.tool_input;
|
|
3050
3394
|
}
|
|
3051
|
-
catch { /* not JSON — keep defaults */ }
|
|
3052
3395
|
}
|
|
3396
|
+
catch { /* not JSON — strict path stays off, advisory still works via payload */ }
|
|
3397
|
+
const DENY_TOOLS = new Set(traits.denyEligibleTools);
|
|
3398
|
+
const shellCommand = traits.shellTool !== null && toolName === traits.shellTool && typeof toolInput.command === 'string'
|
|
3399
|
+
? toolInput.command
|
|
3400
|
+
: null;
|
|
3401
|
+
const { resolveSetting } = await Promise.resolve().then(() => __importStar(require('../config/runtime-settings')));
|
|
3402
|
+
const strict = traits.supportsBeforeToolDeny &&
|
|
3403
|
+
resolveSetting('SPECSHIP_GEMINI_STRICT', cwd, process.env, os.homedir()) === '1';
|
|
3404
|
+
const searchPattern = (typeof toolInput.pattern === 'string' && toolInput.pattern) ||
|
|
3405
|
+
(typeof toolInput.query === 'string' && toolInput.query) ||
|
|
3406
|
+
null;
|
|
3053
3407
|
const { buildSearchIntercept } = await Promise.resolve().then(() => __importStar(require('../activation/search-intercept')));
|
|
3054
|
-
const
|
|
3055
|
-
|
|
3056
|
-
|
|
3408
|
+
const result = buildSearchIntercept({
|
|
3409
|
+
cwd,
|
|
3410
|
+
sessionId: payload.sessionId,
|
|
3411
|
+
transcriptPath: payload.transcriptPath,
|
|
3412
|
+
specshipToolMarkers: traits.specshipToolMarkers,
|
|
3413
|
+
transcriptFormat: traits.transcriptFormat,
|
|
3414
|
+
strict,
|
|
3415
|
+
denyEligible: DENY_TOOLS.has(toolName),
|
|
3416
|
+
shellCommand,
|
|
3417
|
+
searchPattern,
|
|
3418
|
+
});
|
|
3419
|
+
if (result) {
|
|
3420
|
+
if ('decision' in result) {
|
|
3421
|
+
// Gemini BeforeToolHookOutput reads `decision` at the top level
|
|
3422
|
+
// (isBlockingDecision), not inside hookSpecificOutput.
|
|
3423
|
+
process.stdout.write(buildHookDenyDecision(result.reason));
|
|
3424
|
+
}
|
|
3425
|
+
else {
|
|
3426
|
+
process.stdout.write(buildHookAdditionalContext(result.additionalContext, payload.hookEventName, traits.fallbackToolEvent));
|
|
3427
|
+
}
|
|
3057
3428
|
}
|
|
3058
3429
|
}
|
|
3059
3430
|
catch { /* advisory only — never block the matched call (REQ-STEER-004.A2) */ }
|
|
@@ -3155,6 +3526,75 @@ function main() {
|
|
|
3155
3526
|
cg.close();
|
|
3156
3527
|
}
|
|
3157
3528
|
});
|
|
3529
|
+
// @implements REQ-PIPETRUST-007
|
|
3530
|
+
// Pipeline health: release run, npm lag, last JIRA auto-publish, merged run
|
|
3531
|
+
// worktrees. Network checks are time-bounded and skip offline; results are
|
|
3532
|
+
// cached to .specship/pulse.json for the session-start summary.
|
|
3533
|
+
program
|
|
3534
|
+
.command('pulse [path]')
|
|
3535
|
+
.description('Report pipeline health (release run, npm lag, JIRA auto-publish, merged run worktrees); exits non-zero on any failure.')
|
|
3536
|
+
.option('--refresh', 'ignore the cache and re-run every check')
|
|
3537
|
+
.option('--max-age <minutes>', 'serve the cached result when younger than this (default: always re-run)')
|
|
3538
|
+
.option('--json', 'emit JSON')
|
|
3539
|
+
.action(async (pathArg, options) => {
|
|
3540
|
+
const projectRoot = path.resolve(pathArg ?? process.cwd());
|
|
3541
|
+
const { computePulse } = await Promise.resolve().then(() => __importStar(require('../pulse/pulse')));
|
|
3542
|
+
// Resolve a run worktree back to its full run id for the purge hint
|
|
3543
|
+
// (best-effort; falls back to the short id from the branch name).
|
|
3544
|
+
let runIdForWorktree;
|
|
3545
|
+
let close;
|
|
3546
|
+
if ((0, directory_1.isInitialized)(projectRoot)) {
|
|
3547
|
+
try {
|
|
3548
|
+
const { SpecShip } = await loadSpecShip();
|
|
3549
|
+
const cg = await SpecShip.open(projectRoot);
|
|
3550
|
+
close = () => cg.close();
|
|
3551
|
+
const byPath = new Map();
|
|
3552
|
+
const sq = cg.getSpecQueries();
|
|
3553
|
+
for (const run of sq.getAllWorkflowRuns(500)) {
|
|
3554
|
+
if (!run.isolationEnvId)
|
|
3555
|
+
continue;
|
|
3556
|
+
const env = sq.getIsolationEnvById(run.isolationEnvId);
|
|
3557
|
+
if (env?.workingPath)
|
|
3558
|
+
byPath.set(path.resolve(env.workingPath), run.id);
|
|
3559
|
+
}
|
|
3560
|
+
runIdForWorktree = (p) => byPath.get(path.resolve(p));
|
|
3561
|
+
}
|
|
3562
|
+
catch {
|
|
3563
|
+
/* hint falls back to the short id */
|
|
3564
|
+
}
|
|
3565
|
+
}
|
|
3566
|
+
const maxAgeMin = options.maxAge !== undefined ? Number(options.maxAge) : NaN;
|
|
3567
|
+
let result;
|
|
3568
|
+
try {
|
|
3569
|
+
result = await computePulse({
|
|
3570
|
+
projectRoot,
|
|
3571
|
+
refresh: options.refresh,
|
|
3572
|
+
maxAgeMs: Number.isFinite(maxAgeMin) ? maxAgeMin * 60_000 : undefined,
|
|
3573
|
+
writeCache: fs.existsSync(path.join(projectRoot, '.specship')),
|
|
3574
|
+
runIdForWorktree,
|
|
3575
|
+
});
|
|
3576
|
+
}
|
|
3577
|
+
finally {
|
|
3578
|
+
close?.();
|
|
3579
|
+
}
|
|
3580
|
+
if (options.json) {
|
|
3581
|
+
// eslint-disable-next-line no-console
|
|
3582
|
+
console.log(JSON.stringify(result, null, 2));
|
|
3583
|
+
}
|
|
3584
|
+
else {
|
|
3585
|
+
const tag = { ok: 'ok ', fail: 'FAIL', skipped: 'skip' };
|
|
3586
|
+
for (const item of result.items) {
|
|
3587
|
+
// eslint-disable-next-line no-console
|
|
3588
|
+
console.log(`[${tag[item.status]}] ${item.message}${item.hint ? ` — ${item.hint}` : ''}`);
|
|
3589
|
+
}
|
|
3590
|
+
if (result.fromCache) {
|
|
3591
|
+
// eslint-disable-next-line no-console
|
|
3592
|
+
console.log(`(cached ${new Date(result.checkedAt).toISOString()}; --refresh to re-run)`);
|
|
3593
|
+
}
|
|
3594
|
+
}
|
|
3595
|
+
if (result.items.some((i) => i.status === 'fail'))
|
|
3596
|
+
process.exit(1);
|
|
3597
|
+
});
|
|
3158
3598
|
program
|
|
3159
3599
|
.command('drifted [path]')
|
|
3160
3600
|
.description('List spec links in concerning states (drifted, broken, orphaned).')
|
|
@@ -3436,14 +3876,19 @@ function main() {
|
|
|
3436
3876
|
}
|
|
3437
3877
|
});
|
|
3438
3878
|
program
|
|
3439
|
-
.command('workflow <action> [
|
|
3440
|
-
.description('Workflow engine: list | run <name> | resume <runId> | cancel <runId> | approve <runId> | reject <runId> | purge <runId> | runs')
|
|
3879
|
+
.command('workflow <action> [args...]')
|
|
3880
|
+
.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')
|
|
3441
3881
|
.option('-i, --input <kv...>', 'workflow inputs as KEY=VALUE (repeatable)')
|
|
3442
3882
|
.option('--path <projectRoot>', 'project root (default: cwd)')
|
|
3443
3883
|
.option('--comment <text>', 'comment for approve/reject')
|
|
3444
3884
|
.option('--reason <text>', 'reason for cancel/reject')
|
|
3885
|
+
.option('--specs <ids>', 'run-batch: comma-separated requirement ids')
|
|
3886
|
+
.option('--concurrency <n>', 'run-batch: max concurrent runs per wave (default 3)')
|
|
3887
|
+
.option('--dry-run', 'gc: list merged-run worktrees without removing them')
|
|
3888
|
+
.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)')
|
|
3445
3889
|
.option('--json', 'emit JSON where applicable')
|
|
3446
|
-
.action(async (action,
|
|
3890
|
+
.action(async (action, args, options) => {
|
|
3891
|
+
const arg = args?.[0];
|
|
3447
3892
|
const projectRoot = path.resolve(options.path ?? process.cwd());
|
|
3448
3893
|
if (!(0, directory_1.isInitialized)(projectRoot)) {
|
|
3449
3894
|
error(`SpecShip not initialized in ${projectRoot}. Run \`specship init -i\` first.`);
|
|
@@ -3451,6 +3896,7 @@ function main() {
|
|
|
3451
3896
|
}
|
|
3452
3897
|
const { SpecShip } = await loadSpecShip();
|
|
3453
3898
|
const { discoverWorkflows, loadWorkflowByName } = await Promise.resolve().then(() => __importStar(require('../workflows/discovery')));
|
|
3899
|
+
const { resolveWorkflowAgent } = await Promise.resolve().then(() => __importStar(require('../workflows/agents')));
|
|
3454
3900
|
const { WorkflowExecutor } = await Promise.resolve().then(() => __importStar(require('../workflows/executor')));
|
|
3455
3901
|
const { WorktreeProvider } = await Promise.resolve().then(() => __importStar(require('../isolation/worktree')));
|
|
3456
3902
|
const { handleJiraRunCompletion } = await Promise.resolve().then(() => __importStar(require('../mcp/jira-tools')));
|
|
@@ -3541,9 +3987,18 @@ function main() {
|
|
|
3541
3987
|
inputs[inp.name] = inp.default ?? '';
|
|
3542
3988
|
}
|
|
3543
3989
|
}
|
|
3990
|
+
// Unknown agent names fail here, before a run exists (REQ-AGENTADAPT-006.A4).
|
|
3991
|
+
try {
|
|
3992
|
+
resolveWorkflowAgent({ flag: options.agent, projectRoot });
|
|
3993
|
+
}
|
|
3994
|
+
catch (err) {
|
|
3995
|
+
error(err instanceof Error ? err.message : String(err));
|
|
3996
|
+
process.exit(1);
|
|
3997
|
+
}
|
|
3544
3998
|
const result = await executor.start(loaded.workflow, {
|
|
3545
3999
|
projectRoot,
|
|
3546
4000
|
inputs,
|
|
4001
|
+
agent: options.agent,
|
|
3547
4002
|
variables: {
|
|
3548
4003
|
ARTIFACTS_DIR: path.join((0, directory_1.getSpecShipDir)(projectRoot), 'artifacts'),
|
|
3549
4004
|
CONTEXT: projectRoot,
|
|
@@ -3664,6 +4119,208 @@ function main() {
|
|
|
3664
4119
|
: `Run ${arg} had no worktree — nothing to remove. Artifacts and the run record are kept.`);
|
|
3665
4120
|
break;
|
|
3666
4121
|
}
|
|
4122
|
+
case 'run-batch': {
|
|
4123
|
+
// REQ-THRU-001 — one run per requirement, overlapping files serialized.
|
|
4124
|
+
if (!arg) {
|
|
4125
|
+
error('workflow run-batch requires a workflow name');
|
|
4126
|
+
process.exit(1);
|
|
4127
|
+
}
|
|
4128
|
+
const loaded = loadWorkflowByName(projectRoot, arg);
|
|
4129
|
+
if (!loaded) {
|
|
4130
|
+
error(`Workflow "${arg}" not found. Use \`specship workflow list\` to see available workflows.`);
|
|
4131
|
+
process.exit(1);
|
|
4132
|
+
}
|
|
4133
|
+
const specIds = (options.specs ?? '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
4134
|
+
if (specIds.length === 0) {
|
|
4135
|
+
error('workflow run-batch requires --specs <ID,ID,...>');
|
|
4136
|
+
process.exit(1);
|
|
4137
|
+
}
|
|
4138
|
+
const concurrency = options.concurrency !== undefined ? Number(options.concurrency) : undefined;
|
|
4139
|
+
if (concurrency !== undefined && (!Number.isInteger(concurrency) || concurrency < 1)) {
|
|
4140
|
+
error('--concurrency must be a positive integer');
|
|
4141
|
+
process.exit(1);
|
|
4142
|
+
}
|
|
4143
|
+
const baseInputs = {};
|
|
4144
|
+
for (const kv of options.input ?? []) {
|
|
4145
|
+
const eqIdx = kv.indexOf('=');
|
|
4146
|
+
if (eqIdx <= 0) {
|
|
4147
|
+
error(`--input must be KEY=VALUE (got "${kv}")`);
|
|
4148
|
+
process.exit(1);
|
|
4149
|
+
}
|
|
4150
|
+
baseInputs[kv.slice(0, eqIdx)] = kv.slice(eqIdx + 1);
|
|
4151
|
+
}
|
|
4152
|
+
for (const inp of loaded.workflow.inputs ?? []) {
|
|
4153
|
+
if (inp.name !== 'SPEC_ID' && !(inp.name in baseInputs)) {
|
|
4154
|
+
if (inp.required) {
|
|
4155
|
+
error(`Required input missing: --input ${inp.name}=...`);
|
|
4156
|
+
process.exit(1);
|
|
4157
|
+
}
|
|
4158
|
+
baseInputs[inp.name] = inp.default ?? '';
|
|
4159
|
+
}
|
|
4160
|
+
}
|
|
4161
|
+
try {
|
|
4162
|
+
resolveWorkflowAgent({ flag: options.agent, projectRoot });
|
|
4163
|
+
}
|
|
4164
|
+
catch (err) {
|
|
4165
|
+
error(err instanceof Error ? err.message : String(err));
|
|
4166
|
+
process.exit(1);
|
|
4167
|
+
}
|
|
4168
|
+
const { runBatch, TERMINAL_RUN_STATUSES, DEFAULT_BATCH_CONCURRENCY } = await Promise.resolve().then(() => __importStar(require('../workflows/batch')));
|
|
4169
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
4170
|
+
let batch;
|
|
4171
|
+
try {
|
|
4172
|
+
batch = await runBatch({
|
|
4173
|
+
projectRoot,
|
|
4174
|
+
specIds,
|
|
4175
|
+
concurrency,
|
|
4176
|
+
startRun: async (specId) => {
|
|
4177
|
+
const result = await executor.start(loaded.workflow, {
|
|
4178
|
+
projectRoot,
|
|
4179
|
+
agent: options.agent,
|
|
4180
|
+
inputs: { ...baseInputs, SPEC_ID: specId },
|
|
4181
|
+
variables: {
|
|
4182
|
+
ARTIFACTS_DIR: path.join((0, directory_1.getSpecShipDir)(projectRoot), 'artifacts'),
|
|
4183
|
+
CONTEXT: projectRoot,
|
|
4184
|
+
},
|
|
4185
|
+
});
|
|
4186
|
+
return { runId: result.run.id, status: result.run.status };
|
|
4187
|
+
},
|
|
4188
|
+
// A run paused at an approval still holds its files: wait for a
|
|
4189
|
+
// terminal status (approve + resume it from another terminal).
|
|
4190
|
+
waitForRun: async (runId, status) => {
|
|
4191
|
+
let current = status;
|
|
4192
|
+
let announced = false;
|
|
4193
|
+
while (!TERMINAL_RUN_STATUSES.has(current)) {
|
|
4194
|
+
if (current === 'paused' && !announced) {
|
|
4195
|
+
// eslint-disable-next-line no-console
|
|
4196
|
+
console.log(` ${runId} is paused — approve it with \`specship workflow approve ${runId}\`, then \`specship workflow resume ${runId}\``);
|
|
4197
|
+
announced = true;
|
|
4198
|
+
}
|
|
4199
|
+
await sleep(5000);
|
|
4200
|
+
current = specQueries.getWorkflowRunById(runId)?.status ?? 'failed';
|
|
4201
|
+
}
|
|
4202
|
+
return current;
|
|
4203
|
+
},
|
|
4204
|
+
onPlan: (waves) => {
|
|
4205
|
+
if (options.json)
|
|
4206
|
+
return;
|
|
4207
|
+
// eslint-disable-next-line no-console
|
|
4208
|
+
console.log(`Serialization: ${waves.length} wave(s), concurrency ${concurrency ?? DEFAULT_BATCH_CONCURRENCY}`);
|
|
4209
|
+
waves.forEach((w, i) => {
|
|
4210
|
+
// eslint-disable-next-line no-console
|
|
4211
|
+
console.log(` wave ${i + 1}: ${w.join(', ')}`);
|
|
4212
|
+
});
|
|
4213
|
+
},
|
|
4214
|
+
onRunStarted: (r) => {
|
|
4215
|
+
if (options.json)
|
|
4216
|
+
return;
|
|
4217
|
+
// eslint-disable-next-line no-console
|
|
4218
|
+
console.log(` ${r.specId} → run ${r.runId} [${r.status}] (wave ${r.wave + 1})`);
|
|
4219
|
+
},
|
|
4220
|
+
});
|
|
4221
|
+
}
|
|
4222
|
+
catch (err) {
|
|
4223
|
+
error(err instanceof Error ? err.message : String(err));
|
|
4224
|
+
process.exit(1);
|
|
4225
|
+
}
|
|
4226
|
+
if (options.json) {
|
|
4227
|
+
// eslint-disable-next-line no-console
|
|
4228
|
+
console.log(JSON.stringify(batch, null, 2));
|
|
4229
|
+
}
|
|
4230
|
+
else {
|
|
4231
|
+
// eslint-disable-next-line no-console
|
|
4232
|
+
console.log('Runs:');
|
|
4233
|
+
for (const r of batch.runs) {
|
|
4234
|
+
// eslint-disable-next-line no-console
|
|
4235
|
+
console.log(` ${r.runId} ${r.specId} [${r.status}]`);
|
|
4236
|
+
}
|
|
4237
|
+
}
|
|
4238
|
+
if (batch.runs.some((r) => r.status === 'failed'))
|
|
4239
|
+
process.exit(1);
|
|
4240
|
+
break;
|
|
4241
|
+
}
|
|
4242
|
+
case 'merge-queue': {
|
|
4243
|
+
// REQ-THRU-002 — merge completed runs in order, re-checking each merge.
|
|
4244
|
+
const runIds = args ?? [];
|
|
4245
|
+
if (runIds.length === 0) {
|
|
4246
|
+
error('workflow merge-queue requires one or more runIds');
|
|
4247
|
+
process.exit(1);
|
|
4248
|
+
}
|
|
4249
|
+
const { runMergeQueue } = await Promise.resolve().then(() => __importStar(require('../workflows/merge-queue')));
|
|
4250
|
+
const result = runMergeQueue({
|
|
4251
|
+
projectRoot,
|
|
4252
|
+
runIds,
|
|
4253
|
+
getRun: (id) => {
|
|
4254
|
+
const run = specQueries.getWorkflowRunById(id);
|
|
4255
|
+
if (!run)
|
|
4256
|
+
return null;
|
|
4257
|
+
const env = run.isolationEnvId ? specQueries.getIsolationEnvById(run.isolationEnvId) : null;
|
|
4258
|
+
return { id: run.id, status: run.status, branch: env?.branchName };
|
|
4259
|
+
},
|
|
4260
|
+
});
|
|
4261
|
+
if (options.json) {
|
|
4262
|
+
// eslint-disable-next-line no-console
|
|
4263
|
+
console.log(JSON.stringify(result, null, 2));
|
|
4264
|
+
}
|
|
4265
|
+
else {
|
|
4266
|
+
for (const id of result.merged) {
|
|
4267
|
+
// eslint-disable-next-line no-console
|
|
4268
|
+
console.log(` merged ${id}`);
|
|
4269
|
+
}
|
|
4270
|
+
// eslint-disable-next-line no-console
|
|
4271
|
+
console.log(result.stoppedAt
|
|
4272
|
+
? `Stopped at run ${result.stoppedAt} (${result.reason}): ${result.detail ?? ''}`
|
|
4273
|
+
: `Merge queue done — ${result.merged.length} run(s) merged.`);
|
|
4274
|
+
}
|
|
4275
|
+
if (result.stoppedAt)
|
|
4276
|
+
process.exit(1);
|
|
4277
|
+
break;
|
|
4278
|
+
}
|
|
4279
|
+
case 'gc': {
|
|
4280
|
+
// REQ-THRU-007 — purge worktrees of merged runs; records + artifacts kept.
|
|
4281
|
+
const { collectMergedRuns } = await Promise.resolve().then(() => __importStar(require('../workflows/gc')));
|
|
4282
|
+
const result = collectMergedRuns({
|
|
4283
|
+
projectRoot,
|
|
4284
|
+
dryRun: options.dryRun === true,
|
|
4285
|
+
listRuns: () => specQueries.getAllWorkflowRuns(1000).flatMap((run) => {
|
|
4286
|
+
if (!run.isolationEnvId)
|
|
4287
|
+
return [];
|
|
4288
|
+
const env = specQueries.getIsolationEnvById(run.isolationEnvId);
|
|
4289
|
+
if (!env)
|
|
4290
|
+
return [];
|
|
4291
|
+
return [{ id: run.id, status: run.status, branch: env.branchName, workingPath: env.workingPath }];
|
|
4292
|
+
}),
|
|
4293
|
+
purge: (runId) => {
|
|
4294
|
+
executor.purge(runId);
|
|
4295
|
+
},
|
|
4296
|
+
});
|
|
4297
|
+
if (options.json) {
|
|
4298
|
+
// eslint-disable-next-line no-console
|
|
4299
|
+
console.log(JSON.stringify(result, null, 2));
|
|
4300
|
+
}
|
|
4301
|
+
else if (!result.defaultBranch) {
|
|
4302
|
+
// eslint-disable-next-line no-console
|
|
4303
|
+
console.log('Default branch unknown (no origin HEAD, main or master) — nothing collected.');
|
|
4304
|
+
}
|
|
4305
|
+
else {
|
|
4306
|
+
const verb = result.dryRun ? 'would remove' : 'removed';
|
|
4307
|
+
for (const c of result.candidates) {
|
|
4308
|
+
// eslint-disable-next-line no-console
|
|
4309
|
+
console.log(` ${verb} ${c.runId} ${c.branch} (${c.workingPath})`);
|
|
4310
|
+
}
|
|
4311
|
+
for (const s of result.skipped) {
|
|
4312
|
+
// eslint-disable-next-line no-console
|
|
4313
|
+
console.log(` kept ${s.runId} — ${s.reason}`);
|
|
4314
|
+
}
|
|
4315
|
+
// eslint-disable-next-line no-console
|
|
4316
|
+
console.log(result.candidates.length === 0
|
|
4317
|
+
? `No merged-run worktrees to collect (default branch: ${result.defaultBranch}).`
|
|
4318
|
+
: result.dryRun
|
|
4319
|
+
? `${result.candidates.length} worktree(s) would be removed. Run without --dry-run to collect.`
|
|
4320
|
+
: `${result.purged.length} worktree(s) removed. Run records and artifacts are kept.`);
|
|
4321
|
+
}
|
|
4322
|
+
break;
|
|
4323
|
+
}
|
|
3667
4324
|
case 'runs': {
|
|
3668
4325
|
const runs = specQueries.getAllWorkflowRuns(50);
|
|
3669
4326
|
if (options.json) {
|
|
@@ -3686,7 +4343,7 @@ function main() {
|
|
|
3686
4343
|
break;
|
|
3687
4344
|
}
|
|
3688
4345
|
default:
|
|
3689
|
-
error(`Unknown workflow action "${action}". Use: list | run | resume | cancel | approve | reject | purge | runs`);
|
|
4346
|
+
error(`Unknown workflow action "${action}". Use: list | run | run-batch | merge-queue | gc | resume | cancel | approve | reject | purge | runs`);
|
|
3690
4347
|
process.exit(1);
|
|
3691
4348
|
}
|
|
3692
4349
|
}
|
|
@@ -4087,6 +4744,67 @@ function main() {
|
|
|
4087
4744
|
cg.close();
|
|
4088
4745
|
}
|
|
4089
4746
|
});
|
|
4747
|
+
/**
|
|
4748
|
+
* specship jira publish --pending — the explicit, one-batch publish of every
|
|
4749
|
+
* requirement sync held back (REQ-THRU-006). Sync only publishes when
|
|
4750
|
+
* `jira.autoPublish: true` is set in specship.config.json; otherwise it
|
|
4751
|
+
* reports the pending count and points here.
|
|
4752
|
+
*/
|
|
4753
|
+
jira
|
|
4754
|
+
.command('publish')
|
|
4755
|
+
.description('Publish specs to the bound JIRA project in one explicit batch (--pending: every requirement sync held back).')
|
|
4756
|
+
.option('--pending', 'publish every requirement with no JIRA issue yet or whose content changed')
|
|
4757
|
+
.option('--dry-run', 'list what would be published without writing to JIRA')
|
|
4758
|
+
.option('--path <projectRoot>', 'project root (default: cwd)')
|
|
4759
|
+
.action(async (options) => {
|
|
4760
|
+
const projectRoot = path.resolve(options.path ?? process.cwd());
|
|
4761
|
+
if (!options.pending) {
|
|
4762
|
+
error('Nothing to publish: pass --pending to publish every requirement sync held back.');
|
|
4763
|
+
process.exit(1);
|
|
4764
|
+
}
|
|
4765
|
+
if (!(0, directory_1.isInitialized)(projectRoot)) {
|
|
4766
|
+
error(`SpecShip not initialized in ${projectRoot}. Run \`specship init -i\` first.`);
|
|
4767
|
+
process.exit(1);
|
|
4768
|
+
}
|
|
4769
|
+
const { SpecShip } = await loadSpecShip();
|
|
4770
|
+
const { publishPendingSpecs, runAutoPublish, summarizeAutoPublishReport, countAutoPublishReport } = await Promise.resolve().then(() => __importStar(require('../jira/auto-publish')));
|
|
4771
|
+
const cg = await SpecShip.open(projectRoot);
|
|
4772
|
+
try {
|
|
4773
|
+
const deps = { specQueries: cg.getSpecQueries(), projectRoot };
|
|
4774
|
+
const report = options.dryRun
|
|
4775
|
+
? await runAutoPublish(deps, { explicit: true, dryRun: true })
|
|
4776
|
+
: await publishPendingSpecs(deps);
|
|
4777
|
+
if (report.skipped === 'unbound') {
|
|
4778
|
+
error('No JIRA binding in specship.config.json — run `specship jira bind` first.');
|
|
4779
|
+
process.exit(1);
|
|
4780
|
+
}
|
|
4781
|
+
if (options.dryRun) {
|
|
4782
|
+
const pending = report.results.filter((r) => r.status === 'pending');
|
|
4783
|
+
// eslint-disable-next-line no-console
|
|
4784
|
+
console.log(`${pending.length} spec(s) would be published:`);
|
|
4785
|
+
// eslint-disable-next-line no-console
|
|
4786
|
+
for (const r of pending)
|
|
4787
|
+
console.log(` ${r.specId}${r.jiraKey ? ` (${r.jiraKey})` : ''}`);
|
|
4788
|
+
return;
|
|
4789
|
+
}
|
|
4790
|
+
const line = summarizeAutoPublishReport(report);
|
|
4791
|
+
// eslint-disable-next-line no-console
|
|
4792
|
+
if (line)
|
|
4793
|
+
console.log(line.replace(/^JIRA auto-publish/, 'JIRA publish'));
|
|
4794
|
+
for (const r of report.results) {
|
|
4795
|
+
if (r.status === 'failed') {
|
|
4796
|
+
// eslint-disable-next-line no-console
|
|
4797
|
+
console.log(`JIRA: ${r.specId} failed — ${r.note ?? 'unknown error'}`);
|
|
4798
|
+
}
|
|
4799
|
+
}
|
|
4800
|
+
if (report.skipped === 'no-credentials' || countAutoPublishReport(report).failed > 0) {
|
|
4801
|
+
process.exitCode = 1;
|
|
4802
|
+
}
|
|
4803
|
+
}
|
|
4804
|
+
finally {
|
|
4805
|
+
cg.close();
|
|
4806
|
+
}
|
|
4807
|
+
});
|
|
4090
4808
|
/**
|
|
4091
4809
|
* specship jira reconcile — detect JIRA-side edits and (with --apply) fold
|
|
4092
4810
|
* them back into the spec (REQ-JIRATEAM-005). Preview is read-only and the
|