@specship/specship-win32-arm64 0.11.7 → 0.11.9

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.
Files changed (100) hide show
  1. package/lib/dist/bin/specship.js +139 -13
  2. package/lib/dist/bin/specship.js.map +1 -1
  3. package/lib/dist/commands/{ss-check.md → specship/check.md} +5 -0
  4. package/lib/dist/commands/specship/spec.md +338 -0
  5. package/lib/dist/db/migrations.d.ts.map +1 -1
  6. package/lib/dist/db/migrations.js +1 -0
  7. package/lib/dist/db/migrations.js.map +1 -1
  8. package/lib/dist/enforce/enforce.d.ts +15 -0
  9. package/lib/dist/enforce/enforce.d.ts.map +1 -1
  10. package/lib/dist/enforce/enforce.js +43 -1
  11. package/lib/dist/enforce/enforce.js.map +1 -1
  12. package/lib/dist/extraction/index.d.ts +6 -0
  13. package/lib/dist/extraction/index.d.ts.map +1 -1
  14. package/lib/dist/extraction/index.js.map +1 -1
  15. package/lib/dist/index.d.ts +10 -2
  16. package/lib/dist/index.d.ts.map +1 -1
  17. package/lib/dist/index.js +54 -10
  18. package/lib/dist/index.js.map +1 -1
  19. package/lib/dist/installer/index.d.ts.map +1 -1
  20. package/lib/dist/installer/index.js +35 -0
  21. package/lib/dist/installer/index.js.map +1 -1
  22. package/lib/dist/installer/instructions-template.d.ts +2 -2
  23. package/lib/dist/installer/instructions-template.js +5 -5
  24. package/lib/dist/installer/instructions-template.js.map +1 -1
  25. package/lib/dist/installer/targets/claude.d.ts +16 -4
  26. package/lib/dist/installer/targets/claude.d.ts.map +1 -1
  27. package/lib/dist/installer/targets/claude.js +99 -21
  28. package/lib/dist/installer/targets/claude.js.map +1 -1
  29. package/lib/dist/mcp/designer-tools.js +1 -1
  30. package/lib/dist/mcp/designer-tools.js.map +1 -1
  31. package/lib/dist/mcp/server-instructions.d.ts +1 -1
  32. package/lib/dist/mcp/server-instructions.d.ts.map +1 -1
  33. package/lib/dist/mcp/server-instructions.js +1 -1
  34. package/lib/dist/mcp/spec-tools.d.ts +10 -0
  35. package/lib/dist/mcp/spec-tools.d.ts.map +1 -1
  36. package/lib/dist/mcp/spec-tools.js +231 -5
  37. package/lib/dist/mcp/spec-tools.js.map +1 -1
  38. package/lib/dist/mcp/tools.d.ts +12 -0
  39. package/lib/dist/mcp/tools.d.ts.map +1 -1
  40. package/lib/dist/mcp/tools.js +34 -1
  41. package/lib/dist/mcp/tools.js.map +1 -1
  42. package/lib/dist/resolution/brief-link-resolver.d.ts +20 -0
  43. package/lib/dist/resolution/brief-link-resolver.d.ts.map +1 -1
  44. package/lib/dist/resolution/brief-link-resolver.js +65 -1
  45. package/lib/dist/resolution/brief-link-resolver.js.map +1 -1
  46. package/lib/dist/resolution/domain-gap-seed.d.ts +1 -1
  47. package/lib/dist/resolution/domain-gap-seed.js +1 -1
  48. package/lib/dist/resolution/spec-link-resolver.d.ts +16 -2
  49. package/lib/dist/resolution/spec-link-resolver.d.ts.map +1 -1
  50. package/lib/dist/resolution/spec-link-resolver.js +19 -1
  51. package/lib/dist/resolution/spec-link-resolver.js.map +1 -1
  52. package/lib/dist/server/chat/classify.js +28 -8
  53. package/lib/dist/server/ingest/ingestor.js +17 -17
  54. package/lib/dist/server/ingest/pricing-backfill.js +55 -0
  55. package/lib/dist/server/ingest/pricing.js +3 -0
  56. package/lib/dist/server/ingest/project-paths.js +152 -0
  57. package/lib/dist/server/project-registry.js +15 -2
  58. package/lib/dist/server/routes/chat-answer.js +11 -0
  59. package/lib/dist/server/routes/claude.js +34 -13
  60. package/lib/dist/server/routes/events.js +20 -1
  61. package/lib/dist/server/routes/projects.js +6 -3
  62. package/lib/dist/server/routes/spec.js +22 -2
  63. package/lib/dist/server/server.js +21 -2
  64. package/lib/dist/web/chunk-AHI5XEVX.js +1 -0
  65. package/lib/dist/web/chunk-AS2GV63A.js +1 -0
  66. package/lib/dist/web/chunk-CGVC666W.js +1 -0
  67. package/lib/dist/web/chunk-KYSTCHAH.js +1 -0
  68. package/lib/dist/web/{chunk-E27QAKAB.js → chunk-M42QOYAE.js} +1 -1
  69. package/lib/dist/web/chunk-ML5ULGHY.js +1 -0
  70. package/lib/dist/web/{chunk-HPKNOV45.js → chunk-OTQR5VFM.js} +1 -1
  71. package/lib/dist/web/chunk-PS6HRYQQ.js +3 -0
  72. package/lib/dist/web/chunk-S5J5WQXB.js +6 -0
  73. package/lib/dist/web/chunk-W5TEIONI.js +2 -0
  74. package/lib/dist/web/chunk-XKG5X37F.js +1 -0
  75. package/lib/dist/web/index.html +2 -2
  76. package/lib/dist/web/main-JT2RR3JS.js +1 -0
  77. package/lib/dist/web/styles-ATJUD7TT.css +1 -0
  78. package/lib/dist/workflows/defaults/claude-design-implement.yaml +6 -6
  79. package/lib/dist/workflows/defaults/spec-author.yaml +1 -1
  80. package/lib/dist/workflows/defaults/spec-implement.yaml +41 -9
  81. package/lib/node_modules/.package-lock.json +1 -1
  82. package/lib/package.json +1 -1
  83. package/package.json +1 -1
  84. package/lib/dist/commands/ss-behaviour.md +0 -116
  85. package/lib/dist/commands/ss-design-implement.md +0 -84
  86. package/lib/dist/commands/ss-design-loop.md +0 -125
  87. package/lib/dist/commands/ss-spec.md +0 -118
  88. package/lib/dist/web/chunk-7MUNFW56.js +0 -1
  89. package/lib/dist/web/chunk-CYXQILZI.js +0 -1
  90. package/lib/dist/web/chunk-F5TBUYDH.js +0 -1
  91. package/lib/dist/web/chunk-JPSYO24K.js +0 -1
  92. package/lib/dist/web/chunk-LIFS4XH2.js +0 -6
  93. package/lib/dist/web/chunk-MADMRWIK.js +0 -3
  94. package/lib/dist/web/chunk-N5WSFE4J.js +0 -2
  95. package/lib/dist/web/chunk-UYOKO7C5.js +0 -1
  96. package/lib/dist/web/chunk-YNYVOPWF.js +0 -1
  97. package/lib/dist/web/chunk-YXYPTZ6E.js +0 -1
  98. package/lib/dist/web/main-5B4RR3BC.js +0 -1
  99. package/lib/dist/web/styles-KSOPUVDA.css +0 -1
  100. /package/lib/dist/commands/{ss-explore.md → specship/explore.md} +0 -0
@@ -115,11 +115,79 @@ async function loadServerPackage() {
115
115
  * The JSONL ingest watcher now ships inside `@specship/specship-server`
116
116
  * itself — `createServer({ ingest: true })` starts it. No separate package.
117
117
  */
118
+ /**
119
+ * Claude Code's slug encoding (every non-alphanumeric char → '-') is lossy,
120
+ * so decoding by mapping '-' back to '/' mangles any path containing
121
+ * hyphens/dots/underscores. Recover real paths from `~/.claude.json` (its
122
+ * `projects` object is keyed by real absolute paths), then from a `"cwd"`
123
+ * value in the slug's newest transcript, before falling back to the lossy
124
+ * decode. Mirrors `packages/server/src/ingest/project-paths.ts`
125
+ * (REQ-SLUGRES-004) — the server package depends on this one, so the CLI
126
+ * can't import it.
127
+ */
128
+ function buildClaudeSlugIndex() {
129
+ const bySlug = new Map();
130
+ try {
131
+ const raw = fs.readFileSync(path.join(os.homedir(), '.claude.json'), 'utf8');
132
+ const parsed = JSON.parse(raw);
133
+ for (const realPath of Object.keys(parsed.projects ?? {})) {
134
+ bySlug.set(realPath.replace(/[^A-Za-z0-9]/g, '-'), realPath);
135
+ }
136
+ }
137
+ catch { /* missing or malformed — sniff/fallback below still work */ }
138
+ return bySlug;
139
+ }
140
+ function sniffClaudeSlugCwd(claudeRoot, slug) {
141
+ const dir = path.join(claudeRoot, slug);
142
+ let newest = null;
143
+ try {
144
+ for (const f of fs.readdirSync(dir, { withFileTypes: true })) {
145
+ if (!f.isFile() || !f.name.toLowerCase().endsWith('.jsonl'))
146
+ continue;
147
+ const st = fs.statSync(path.join(dir, f.name));
148
+ if (!newest || st.mtimeMs > newest.mtimeMs)
149
+ newest = { file: f.name, mtimeMs: st.mtimeMs };
150
+ }
151
+ }
152
+ catch {
153
+ return null;
154
+ }
155
+ if (!newest)
156
+ return null;
157
+ try {
158
+ const fd = fs.openSync(path.join(dir, newest.file), 'r');
159
+ let chunk;
160
+ try {
161
+ const buf = Buffer.alloc(64 * 1024);
162
+ const read = fs.readSync(fd, buf, 0, buf.length, 0);
163
+ chunk = buf.toString('utf8', 0, read);
164
+ }
165
+ finally {
166
+ fs.closeSync(fd);
167
+ }
168
+ const m = /"cwd"\s*:\s*"((?:[^"\\]|\\.)*)"/.exec(chunk);
169
+ if (!m)
170
+ return null;
171
+ let cwd = JSON.parse(`"${m[1]}"`);
172
+ // Accept the cwd or an ancestor of it — sessions may record a subdir.
173
+ for (;;) {
174
+ if (cwd.replace(/[^A-Za-z0-9]/g, '-') === slug)
175
+ return cwd;
176
+ const parent = path.dirname(cwd);
177
+ if (parent === cwd)
178
+ return null;
179
+ cwd = parent;
180
+ }
181
+ }
182
+ catch {
183
+ return null;
184
+ }
185
+ }
118
186
  /**
119
187
  * Auto-pick a default project for `specship serve --ui` when the user
120
188
  * didn't pass `-p` and the cwd isn't initialized either. Walks
121
189
  * `~/.claude/projects/*` (Claude Code's per-project transcript dirs),
122
- * decodes each slug back to a real path, and returns the most-recently-
190
+ * resolves each slug back to a real path, and returns the most-recently-
123
191
  * touched one that's still on disk AND has been `specship init`'d.
124
192
  *
125
193
  * Returns null when nothing qualifies — the server boots projectless and
@@ -134,13 +202,15 @@ async function pickRecentInitializedProject() {
134
202
  catch {
135
203
  return null;
136
204
  }
205
+ const slugIndex = buildClaudeSlugIndex();
137
206
  const candidates = [];
138
207
  for (const ent of entries) {
139
208
  if (!ent.isDirectory())
140
209
  continue;
141
210
  const slug = ent.name;
142
- // Same encoding the server's decodeProjectSlug uses: '-' → '/'.
143
- const decoded = slug.startsWith('-') ? '/' + slug.slice(1).replace(/-/g, '/') : slug;
211
+ const decoded = slugIndex.get(slug) ??
212
+ sniffClaudeSlugCwd(claudeRoot, slug) ??
213
+ (slug.startsWith('-') ? '/' + slug.slice(1).replace(/-/g, '/') : slug);
144
214
  try {
145
215
  const st = fs.statSync(decoded);
146
216
  if (!st.isDirectory())
@@ -769,9 +839,28 @@ function main() {
769
839
  program
770
840
  .command('sync [path]')
771
841
  .description('Sync changes since last index')
772
- .option('-q, --quiet', 'Suppress output (for git hooks)')
842
+ .option('-q, --quiet', 'Suppress output (for git hooks); drift-transition notices still print')
843
+ .option('--drift-summary', 'After syncing, print a one-line drifted-link summary when the queue is non-empty (for the SessionStart hook)')
773
844
  .action(async (pathArg, options) => {
774
845
  const projectPath = resolveProjectPath(pathArg);
846
+ // One line per link that TRANSITIONED into drifted this sync
847
+ // (REQ-DRIFT-PUSH-001). Deliberately printed even under --quiet — the
848
+ // auto-sync hook runs with --quiet, and these notices are its payload.
849
+ const pushDriftNotices = (result) => {
850
+ for (const t of result.driftedTransitions ?? []) {
851
+ console.log(`⚠ spec drift: ${t.specId} ${t.fromState}→drifted (${t.symbol} ${t.axis === 'spec' ? 'spec changed' : 'changed'}) — ` +
852
+ `re-assert with specship_link_assert, or run /specship:check fix ${t.specId}`);
853
+ }
854
+ };
855
+ // One-line drift-queue summary, only when non-empty (REQ-DRIFT-PUSH-002).
856
+ const pushDriftSummary = (cg) => {
857
+ if (!options.driftSummary)
858
+ return;
859
+ const drifted = cg.getSpecQueries().getLinksByState(['drifted']).length;
860
+ if (drifted > 0) {
861
+ console.log(`⚠ ${drifted} spec link(s) drifted — review with /specship:check drifted`);
862
+ }
863
+ };
775
864
  try {
776
865
  if (!(0, directory_1.isInitialized)(projectPath)) {
777
866
  if (!options.quiet) {
@@ -782,7 +871,9 @@ function main() {
782
871
  const { default: SpecShip } = await loadSpecShip();
783
872
  const cg = await SpecShip.open(projectPath);
784
873
  if (options.quiet) {
785
- await cg.sync();
874
+ const result = await cg.sync();
875
+ pushDriftNotices(result);
876
+ pushDriftSummary(cg);
786
877
  cg.destroy();
787
878
  return;
788
879
  }
@@ -809,6 +900,8 @@ function main() {
809
900
  details.push(`Removed: ${result.filesRemoved}`);
810
901
  clack.log.info(`${details.join(', ')} ${(0, glyphs_1.getGlyphs)().dash} ${formatNumber(result.nodesUpdated)} nodes in ${formatDuration(result.durationMs)}`);
811
902
  }
903
+ pushDriftNotices(result);
904
+ pushDriftSummary(cg);
812
905
  clack.outro('Done');
813
906
  cg.destroy();
814
907
  }
@@ -1450,11 +1543,18 @@ function main() {
1450
1543
  * GATING check fails. Which checks gate vs advise is configured in
1451
1544
  * specship.config.json `enforce.gate`; with no config every check is advisory
1452
1545
  * and the command exits 0 (opt-in — never breaks an existing repo).
1546
+ *
1547
+ * The graduation ramp (REQ-ENFORCE-004): `--strict` gates every check for
1548
+ * one run; `--enable-gate <checks...>` persists gating into the config and
1549
+ * then runs; an advisory run with would-be-gating findings ends by printing
1550
+ * the exact `--enable-gate` command that turns the gate on.
1453
1551
  */
1454
1552
  program
1455
1553
  .command('check [path]')
1456
1554
  .description('Run the enforcement gate (drift + fitness + maintainability + behaviour); exits non-zero on a gating failure')
1457
1555
  .option('-j, --json', 'Output as JSON')
1556
+ .option('--strict', 'Treat every check as gating for this run only (nothing read from or written to config)')
1557
+ .option('--enable-gate <checks...>', 'Persist gating for the named checks into specship.config.json, then run the gate')
1458
1558
  .action(async (pathArg, options) => {
1459
1559
  const projectPath = resolveProjectPath(pathArg);
1460
1560
  try {
@@ -1462,9 +1562,25 @@ function main() {
1462
1562
  error(`SpecShip not initialized in ${projectPath}`);
1463
1563
  process.exit(1);
1464
1564
  }
1565
+ const enforce = await Promise.resolve().then(() => __importStar(require('../enforce/enforce')));
1566
+ // --enable-gate writes the opt-in config, then falls through to a
1567
+ // normal run so the user immediately sees the gate with teeth
1568
+ // (REQ-ENFORCE-004.A1).
1569
+ if (options.enableGate?.length) {
1570
+ const invalid = options.enableGate.filter((c) => !enforce.ALL_CHECKS.includes(c));
1571
+ if (invalid.length) {
1572
+ error(`Unknown check(s): ${invalid.join(', ')}. Valid: ${enforce.ALL_CHECKS.join(', ')}`);
1573
+ process.exit(1);
1574
+ }
1575
+ const enabled = enforce.enableGateChecks(projectPath, options.enableGate);
1576
+ if (enabled.length) {
1577
+ success(`Gating enabled for ${enabled.join(', ')} — written to specship.config.json.`);
1578
+ }
1579
+ }
1465
1580
  const { default: SpecShip } = await loadSpecShip();
1466
1581
  const cg = await SpecShip.open(projectPath);
1467
- const r = cg.getEnforce();
1582
+ // --strict gates everything for this run only (REQ-ENFORCE-004.A2).
1583
+ const r = options.strict ? cg.getEnforce(enforce.strictEnforceConfig()) : cg.getEnforce();
1468
1584
  if (options.json) {
1469
1585
  console.log(JSON.stringify(r, null, 2));
1470
1586
  cg.destroy();
@@ -1486,6 +1602,16 @@ function main() {
1486
1602
  success(r.gatedFailures.length === 0 && r.checks.some((c) => c.gating)
1487
1603
  ? 'All gating checks pass.'
1488
1604
  : 'Pass (no gating checks failed).');
1605
+ // The advisory sell (REQ-ENFORCE-004.A1): findings that would fail
1606
+ // a gated run end the report with the exact opt-in command.
1607
+ const wouldGate = r.checks.filter((c) => !c.gating && !c.passed).map((c) => c.check);
1608
+ if (wouldGate.length) {
1609
+ const total = r.checks
1610
+ .filter((c) => wouldGate.includes(c.check))
1611
+ .reduce((n, c) => n + c.findings.length, 0);
1612
+ console.log(chalk.dim(`\n ${total} advisory finding(s) would fail a gated run.` +
1613
+ `\n Enable: specship check --enable-gate ${wouldGate.join(' ')}\n`));
1614
+ }
1489
1615
  cg.destroy();
1490
1616
  return;
1491
1617
  }
@@ -2184,14 +2310,14 @@ function main() {
2184
2310
  });
2185
2311
  /**
2186
2312
  * specship starter-prompt — print the manufactured first-run flow/impact prompt
2187
- * (ACTIVATION-DOC). The single delivery mechanism for the bare `/ss-explore`
2313
+ * (ACTIVATION-DOC). The single delivery mechanism for the bare `/specship:explore`
2188
2314
  * door: it self-gates (prints nothing if the index can't be queried —
2189
2315
  * REQ-ACTIVATION-004) and self-retires (prints nothing once a real specship
2190
2316
  * lookup has been recorded this session — REQ-ACTIVATION-003).
2191
2317
  */
2192
2318
  program
2193
2319
  .command('starter-prompt [path]')
2194
- .description('Print a suggested first flow/impact prompt for this project (used by the /ss-explore door).')
2320
+ .description('Print a suggested first flow/impact prompt for this project (used by the /specship:explore door).')
2195
2321
  .option('-j, --json', 'Output as JSON')
2196
2322
  .action(async (pathArg, options) => {
2197
2323
  const projectRoot = path.resolve(pathArg ?? process.cwd());
@@ -2278,7 +2404,7 @@ function main() {
2278
2404
  if (shouldNudge(prompt)) {
2279
2405
  const additionalContext = 'This repo uses spec-driven development (SpecShip). Before any brainstorming or ' +
2280
2406
  'planning skill, FIRST invoke spec-author to author the spec under specs/ for this ' +
2281
- 'work — the spec is the contract; implement from it with /ss-spec implement.';
2407
+ 'work — the spec is the contract; implement from it with /specship:spec implement.';
2282
2408
  process.stdout.write(JSON.stringify({ hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext } }) + '\n');
2283
2409
  }
2284
2410
  process.exit(0);
@@ -2370,12 +2496,12 @@ function main() {
2370
2496
  });
2371
2497
  // @implements REQ-DOMAIN-003
2372
2498
  // Thin surface over the read-only gap-seed pass (SpecShip.getDomainGapSeed,
2373
- // REQ-DOMAIN-003) so the `/ss-spec domain` capture command can cite the SAME real
2499
+ // REQ-DOMAIN-003) so the `/specship:spec domain` capture command can cite the SAME real
2374
2500
  // undocumented entities/specs the library computes (REQ-DOMAIN-004.A4) without a
2375
2501
  // new MCP tool (REQ-DOMAIN-005) or a runtime package import. Writes nothing.
2376
2502
  program
2377
2503
  .command('domain-gaps [path]')
2378
- .description('List code entities and specs not yet covered by a domain fact (the domain gap-seed). Feeds the /ss-spec domain capture interview.')
2504
+ .description('List code entities and specs not yet covered by a domain fact (the domain gap-seed). Feeds the /specship:spec domain capture interview.')
2379
2505
  .option('-l, --limit <n>', 'max entities and specs to print in text mode (default: 50)')
2380
2506
  .option('--json', 'emit JSON')
2381
2507
  .action(async (pathArg, options) => {
@@ -2418,7 +2544,7 @@ function main() {
2418
2544
  if (seed.specs.length > limit)
2419
2545
  console.log(` … and ${seed.specs.length - limit} more`);
2420
2546
  }
2421
- console.log(`\nCapture a fact for any of these with \`/ss-spec domain\`.`);
2547
+ console.log(`\nCapture a fact for any of these with \`/specship:spec domain\`.`);
2422
2548
  }
2423
2549
  /* eslint-enable no-console */
2424
2550
  }
@@ -2544,7 +2670,7 @@ function main() {
2544
2670
  }
2545
2671
  else {
2546
2672
  // eslint-disable-next-line no-console
2547
- console.log('No specs or briefs found. Author one with /ss-spec new.');
2673
+ console.log('No specs or briefs found. Author one with /specship:spec new.');
2548
2674
  }
2549
2675
  return;
2550
2676
  }