@specship/specship-win32-arm64 0.19.1 → 0.21.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.
Files changed (70) hide show
  1. package/lib/dist/activation/cheatsheet.d.ts +33 -0
  2. package/lib/dist/activation/cheatsheet.d.ts.map +1 -0
  3. package/lib/dist/activation/cheatsheet.js +60 -0
  4. package/lib/dist/activation/cheatsheet.js.map +1 -0
  5. package/lib/dist/bin/specship.js +393 -2
  6. package/lib/dist/bin/specship.js.map +1 -1
  7. package/lib/dist/commands/specship/spec.md +19 -0
  8. package/lib/dist/db/spec-queries.d.ts.map +1 -1
  9. package/lib/dist/db/spec-queries.js +18 -3
  10. package/lib/dist/db/spec-queries.js.map +1 -1
  11. package/lib/dist/hooks/hooks.json +9 -0
  12. package/lib/dist/index.d.ts +13 -0
  13. package/lib/dist/index.d.ts.map +1 -1
  14. package/lib/dist/index.js +16 -0
  15. package/lib/dist/index.js.map +1 -1
  16. package/lib/dist/installer/targets/claude.d.ts.map +1 -1
  17. package/lib/dist/installer/targets/claude.js +9 -0
  18. package/lib/dist/installer/targets/claude.js.map +1 -1
  19. package/lib/dist/jira/client.d.ts +61 -0
  20. package/lib/dist/jira/client.d.ts.map +1 -1
  21. package/lib/dist/jira/client.js +120 -0
  22. package/lib/dist/jira/client.js.map +1 -1
  23. package/lib/dist/jira/config.d.ts.map +1 -1
  24. package/lib/dist/jira/config.js +7 -1
  25. package/lib/dist/jira/config.js.map +1 -1
  26. package/lib/dist/jira/publish.d.ts +201 -0
  27. package/lib/dist/jira/publish.d.ts.map +1 -0
  28. package/lib/dist/jira/publish.js +0 -0
  29. package/lib/dist/jira/publish.js.map +1 -0
  30. package/lib/dist/jira/spec-writer.d.ts +7 -0
  31. package/lib/dist/jira/spec-writer.d.ts.map +1 -1
  32. package/lib/dist/jira/spec-writer.js +15 -0
  33. package/lib/dist/jira/spec-writer.js.map +1 -1
  34. package/lib/dist/jira/types.d.ts +13 -0
  35. package/lib/dist/jira/types.d.ts.map +1 -1
  36. package/lib/dist/jira/types.js.map +1 -1
  37. package/lib/dist/mcp/jira-tools.d.ts +85 -0
  38. package/lib/dist/mcp/jira-tools.d.ts.map +1 -1
  39. package/lib/dist/mcp/jira-tools.js +308 -2
  40. package/lib/dist/mcp/jira-tools.js.map +1 -1
  41. package/lib/dist/mcp/server-instructions.d.ts +1 -1
  42. package/lib/dist/mcp/server-instructions.d.ts.map +1 -1
  43. package/lib/dist/mcp/server-instructions.js +2 -1
  44. package/lib/dist/mcp/server-instructions.js.map +1 -1
  45. package/lib/dist/mcp/spec-tools.d.ts.map +1 -1
  46. package/lib/dist/mcp/spec-tools.js +92 -1
  47. package/lib/dist/mcp/spec-tools.js.map +1 -1
  48. package/lib/dist/mcp/tools.d.ts.map +1 -1
  49. package/lib/dist/mcp/tools.js +14 -0
  50. package/lib/dist/mcp/tools.js.map +1 -1
  51. package/lib/dist/reflect/index.d.ts +1 -1
  52. package/lib/dist/reflect/index.d.ts.map +1 -1
  53. package/lib/dist/reflect/index.js +2 -1
  54. package/lib/dist/reflect/index.js.map +1 -1
  55. package/lib/dist/reflect/sweep.d.ts +14 -0
  56. package/lib/dist/reflect/sweep.d.ts.map +1 -1
  57. package/lib/dist/reflect/sweep.js +31 -0
  58. package/lib/dist/reflect/sweep.js.map +1 -1
  59. package/lib/dist/resolution/spec-link-resolver.d.ts +12 -2
  60. package/lib/dist/resolution/spec-link-resolver.d.ts.map +1 -1
  61. package/lib/dist/resolution/spec-link-resolver.js +20 -5
  62. package/lib/dist/resolution/spec-link-resolver.js.map +1 -1
  63. package/lib/dist/types.d.ts +8 -0
  64. package/lib/dist/types.d.ts.map +1 -1
  65. package/lib/dist/types.js +12 -1
  66. package/lib/dist/types.js.map +1 -1
  67. package/lib/dist/workflows/defaults/spec-implement.yaml +6 -0
  68. package/lib/node_modules/.package-lock.json +1 -1
  69. package/lib/package.json +1 -1
  70. package/package.json +1 -1
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Session cheat-sheet (CHEATSHEET-DOC).
3
+ *
4
+ * SpecShip's agent-facing guidance ships in the MCP `initialize` response, so
5
+ * Claude always knows how to drive the tools. The human driving the session
6
+ * has no equivalent. This module is the human's map — a short, static
7
+ * capability cheat-sheet printed at session start as a `SessionStart` hook
8
+ * `systemMessage` (the command is `specship cheatsheet`, installed by the
9
+ * installer and shipped in the plugin's `hooks/hooks.json`).
10
+ *
11
+ * Distinct from `starter-prompt` (a dynamic, self-silencing first-run flow
12
+ * prompt) and from the dashboard's improvement tips (`claude_tip_state`) — see
13
+ * the spec's scope note. Noise is bounded by the hook matcher (startup only)
14
+ * and the `SPECSHIP_NO_CHEATSHEET` opt-out below, not by this module.
15
+ */
16
+ /**
17
+ * The cheat-sheet rendered to the user. Names only user-facing commands, tools,
18
+ * and env vars — no internal file paths, symbol names, or benchmark figures
19
+ * (REQ-CHEAT-002.A3), so it reads as a product map rather than engineering
20
+ * notes.
21
+ */
22
+ export declare const CHEATSHEET_TEXT: string;
23
+ /**
24
+ * Build the `SessionStart`-hook payload the `specship cheatsheet` command
25
+ * prints. Returns `{ systemMessage }` (user-visible only — never
26
+ * `additionalContext`, REQ-CHEAT-001.A2), or `null` when the user has opted
27
+ * out via `SPECSHIP_NO_CHEATSHEET` (REQ-CHEAT-004.A1). An unset or empty
28
+ * variable prints normally (REQ-CHEAT-004.A2).
29
+ */
30
+ export declare function buildCheatsheetPayload(env?: NodeJS.ProcessEnv): {
31
+ systemMessage: string;
32
+ } | null;
33
+ //# sourceMappingURL=cheatsheet.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cheatsheet.d.ts","sourceRoot":"","sources":["../../src/activation/cheatsheet.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH;;;;;GAKG;AACH,eAAO,MAAM,eAAe,QAqBhB,CAAC;AAEb;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC;IAAE,aAAa,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAGlC"}
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ /**
3
+ * Session cheat-sheet (CHEATSHEET-DOC).
4
+ *
5
+ * SpecShip's agent-facing guidance ships in the MCP `initialize` response, so
6
+ * Claude always knows how to drive the tools. The human driving the session
7
+ * has no equivalent. This module is the human's map — a short, static
8
+ * capability cheat-sheet printed at session start as a `SessionStart` hook
9
+ * `systemMessage` (the command is `specship cheatsheet`, installed by the
10
+ * installer and shipped in the plugin's `hooks/hooks.json`).
11
+ *
12
+ * Distinct from `starter-prompt` (a dynamic, self-silencing first-run flow
13
+ * prompt) and from the dashboard's improvement tips (`claude_tip_state`) — see
14
+ * the spec's scope note. Noise is bounded by the hook matcher (startup only)
15
+ * and the `SPECSHIP_NO_CHEATSHEET` opt-out below, not by this module.
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.CHEATSHEET_TEXT = void 0;
19
+ exports.buildCheatsheetPayload = buildCheatsheetPayload;
20
+ /**
21
+ * The cheat-sheet rendered to the user. Names only user-facing commands, tools,
22
+ * and env vars — no internal file paths, symbol names, or benchmark figures
23
+ * (REQ-CHEAT-002.A3), so it reads as a product map rather than engineering
24
+ * notes.
25
+ */
26
+ exports.CHEATSHEET_TEXT = [
27
+ '📦 SpecShip — code intelligence + spec-driven flow over the indexed graph',
28
+ '',
29
+ 'The four doors (slash commands):',
30
+ '• /specship:explore — reads door. "how does X work / reach Y", blast radius.',
31
+ '• /specship:spec — intent door. list · new · fast · design · implement · review.',
32
+ '• /specship:check — gate & health door (drift queue, fix, relink, health).',
33
+ '• /specship:learn — lessons door. crystallize what worked into a skill proposal.',
34
+ '',
35
+ 'Explore before you Read/Grep:',
36
+ '• specship_explore is PRIMARY — one call returns verbatim source grouped by file.',
37
+ '• Flow: name both ends (e.g. "mutateElement renderScene") to trace the call path.',
38
+ '• Locate → specship_search · impact → specship_impact · callers/callees too.',
39
+ '',
40
+ 'JIRA: "my JIRA issues", "pick PROJ-123", "start it", "publish REQ-X to JIRA".',
41
+ 'Drift & health: /specship:check · check drifted · check fix <ID> · check health.',
42
+ 'Lessons & memory: /specship:learn; auto-memory has four types (user · feedback ·',
43
+ ' project · reference) that persist across sessions.',
44
+ 'Verify: the gate runs the spec→test→verify chain; specship_link_verify closes a link.',
45
+ '',
46
+ 'Silence this with SPECSHIP_NO_CHEATSHEET=1. specship_status shows index freshness.',
47
+ ].join('\n');
48
+ /**
49
+ * Build the `SessionStart`-hook payload the `specship cheatsheet` command
50
+ * prints. Returns `{ systemMessage }` (user-visible only — never
51
+ * `additionalContext`, REQ-CHEAT-001.A2), or `null` when the user has opted
52
+ * out via `SPECSHIP_NO_CHEATSHEET` (REQ-CHEAT-004.A1). An unset or empty
53
+ * variable prints normally (REQ-CHEAT-004.A2).
54
+ */
55
+ function buildCheatsheetPayload(env = process.env) {
56
+ if (env.SPECSHIP_NO_CHEATSHEET)
57
+ return null;
58
+ return { systemMessage: exports.CHEATSHEET_TEXT };
59
+ }
60
+ //# sourceMappingURL=cheatsheet.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cheatsheet.js","sourceRoot":"","sources":["../../src/activation/cheatsheet.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;GAcG;;;AAsCH,wDAKC;AAzCD;;;;;GAKG;AACU,QAAA,eAAe,GAAG;IAC7B,2EAA2E;IAC3E,EAAE;IACF,kCAAkC;IAClC,+EAA+E;IAC/E,sFAAsF;IACtF,+EAA+E;IAC/E,qFAAqF;IACrF,EAAE;IACF,+BAA+B;IAC/B,mFAAmF;IACnF,mFAAmF;IACnF,8EAA8E;IAC9E,EAAE;IACF,+EAA+E;IAC/E,kFAAkF;IAClF,kFAAkF;IAClF,sDAAsD;IACtD,uFAAuF;IACvF,EAAE;IACF,oFAAoF;CACrF,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAEb;;;;;;GAMG;AACH,SAAgB,sBAAsB,CACpC,MAAyB,OAAO,CAAC,GAAG;IAEpC,IAAI,GAAG,CAAC,sBAAsB;QAAE,OAAO,IAAI,CAAC;IAC5C,OAAO,EAAE,aAAa,EAAE,uBAAe,EAAE,CAAC;AAC5C,CAAC"}
@@ -807,6 +807,41 @@ function main() {
807
807
  `re-assert with specship_link_assert, or run /specship:check fix ${t.specId}`);
808
808
  }
809
809
  };
810
+ // JIRA push (REQ-JIRAPUB-006): a genuine drift transition on a JIRA-backed
811
+ // spec is commented onto its issue — best-effort, transition events only
812
+ // (so an ongoing drift never repeats a comment), never blocks the sync.
813
+ const pushJiraDriftComments = async (cg, result) => {
814
+ const transitions = result.driftedTransitions ?? [];
815
+ if (transitions.length === 0)
816
+ return;
817
+ try {
818
+ const { loadJiraConfig, resolveJiraCredentials } = await Promise.resolve().then(() => __importStar(require('../jira/config')));
819
+ if (!loadJiraConfig())
820
+ return; // not configured → no JIRA calls at all
821
+ const { readSpecJiraKey } = await Promise.resolve().then(() => __importStar(require('../jira/spec-writer')));
822
+ const { JiraClient } = await Promise.resolve().then(() => __importStar(require('../jira/client')));
823
+ const { commentDriftTransitionsOnJira } = await Promise.resolve().then(() => __importStar(require('../jira/publish')));
824
+ const notes = await commentDriftTransitionsOnJira({
825
+ transitions,
826
+ specPathFor: (specId) => {
827
+ const rootId = specId.replace(/\.A\d+$/, '');
828
+ const spec = cg.getSpecQueries().getSpecById(rootId);
829
+ if (!spec)
830
+ return null;
831
+ return path.isAbsolute(spec.sourcePath)
832
+ ? spec.sourcePath
833
+ : path.join(projectPath, spec.sourcePath);
834
+ },
835
+ readKey: readSpecJiraKey,
836
+ makeClient: () => new JiraClient(resolveJiraCredentials()),
837
+ });
838
+ for (const note of notes)
839
+ console.log(`JIRA: ${note}`);
840
+ }
841
+ catch {
842
+ /* a JIRA fault must never fail a sync */
843
+ }
844
+ };
810
845
  // One-line drift-queue summary, only when non-empty (REQ-DRIFT-PUSH-002).
811
846
  const pushDriftSummary = (cg) => {
812
847
  if (!options.driftSummary)
@@ -828,6 +863,7 @@ function main() {
828
863
  if (options.quiet) {
829
864
  const result = await cg.sync();
830
865
  pushDriftNotices(result);
866
+ await pushJiraDriftComments(cg, result);
831
867
  pushDriftSummary(cg);
832
868
  cg.destroy();
833
869
  return;
@@ -856,6 +892,7 @@ function main() {
856
892
  clack.log.info(`${details.join(', ')} ${(0, glyphs_1.getGlyphs)().dash} ${formatNumber(result.nodesUpdated)} nodes in ${formatDuration(result.durationMs)}`);
857
893
  }
858
894
  pushDriftNotices(result);
895
+ await pushJiraDriftComments(cg, result);
859
896
  pushDriftSummary(cg);
860
897
  clack.outro('Done');
861
898
  cg.destroy();
@@ -1356,6 +1393,195 @@ function main() {
1356
1393
  process.exit(1);
1357
1394
  }
1358
1395
  });
1396
+ /**
1397
+ * specship memory — capture lessons to memory and review/manage stored items
1398
+ * (MEMLESSON-DOC). `capture` turns a stated mistake into a human-gated
1399
+ * memory_rule proposal (REQ-MEMLESSON-001); nothing is written until it is
1400
+ * applied (dashboard Improvements / `specship reflect`).
1401
+ */
1402
+ const memory = program
1403
+ .command('memory')
1404
+ .description('Capture lessons to memory and review/manage stored memory items.');
1405
+ memory
1406
+ .command('capture')
1407
+ .description('Capture a lesson/anti-pattern as a human-gated memory rule (lesson text on stdin).')
1408
+ .requiredOption('--title <title>', 'short title for the lesson')
1409
+ .option('--target <target>', 'where apply would write it: "memory" (portable ~/.claude note, default) or "claude-md" (project CLAUDE.md)', 'memory')
1410
+ .option('--path <projectRoot>', 'project root (default: cwd)')
1411
+ .option('-j, --json', 'output the created proposal as JSON')
1412
+ .action(async (options) => {
1413
+ const projectPath = resolveProjectPath(options.path);
1414
+ const target = options.target === 'claude-md' ? 'claude-md' : 'memory';
1415
+ try {
1416
+ if (!(0, directory_1.isInitialized)(projectPath)) {
1417
+ error(`SpecShip not initialized in ${projectPath}`);
1418
+ process.exit(1);
1419
+ }
1420
+ // Lesson content on stdin so a multi-line "mistake + rule to avoid it"
1421
+ // survives shell quoting (mirrors `specship reflect --capture`).
1422
+ const chunks = [];
1423
+ for await (const c of process.stdin)
1424
+ chunks.push(c);
1425
+ const content = Buffer.concat(chunks).toString('utf-8').trim();
1426
+ if (!content) {
1427
+ error('memory capture expects the lesson on stdin (pipe or heredoc): the mistake + the rule to avoid repeating it.');
1428
+ process.exit(1);
1429
+ }
1430
+ const { SpecShip } = await loadSpecShip();
1431
+ const cg = await SpecShip.open(projectPath);
1432
+ const p = cg.reflectCaptureLesson({ title: options.title, content, target });
1433
+ if (options.json) {
1434
+ // eslint-disable-next-line no-console
1435
+ console.log(JSON.stringify(p, null, 2));
1436
+ }
1437
+ else {
1438
+ info(`Captured lesson as proposal ${p.contentHash.slice(0, 12)} (${p.state}): ${p.title}`);
1439
+ info(`Target: ${p.targetKind === 'claude_md' ? 'project CLAUDE.md' : 'portable ~/.claude memory note'} — nothing written yet.`);
1440
+ info('Review + apply from the dashboard Improvements page (preview-diff → confirm), or `specship reflect`.');
1441
+ }
1442
+ cg.destroy();
1443
+ }
1444
+ catch (err) {
1445
+ error(`memory capture failed: ${err instanceof Error ? err.message : String(err)}`);
1446
+ process.exit(1);
1447
+ }
1448
+ });
1449
+ // Body text of a memory_rule proposal's payload (the note or the CLAUDE.md block).
1450
+ function memoryRuleBody(p) {
1451
+ if (p.payload.kind === 'memory_note')
1452
+ return p.payload.note ?? '';
1453
+ if (p.payload.kind === 'claude_md')
1454
+ return p.payload.block ?? '';
1455
+ return '';
1456
+ }
1457
+ memory
1458
+ .command('list')
1459
+ .description('List the memory rules SpecShip has applied (reflect-managed store).')
1460
+ .option('--path <projectRoot>', 'project root (default: cwd)')
1461
+ .option('-j, --json', 'output as JSON')
1462
+ .action(async (options) => {
1463
+ const projectPath = resolveProjectPath(options.path);
1464
+ try {
1465
+ if (!(0, directory_1.isInitialized)(projectPath)) {
1466
+ error(`SpecShip not initialized in ${projectPath}`);
1467
+ process.exit(1);
1468
+ }
1469
+ const { SpecShip } = await loadSpecShip();
1470
+ const cg = await SpecShip.open(projectPath);
1471
+ const items = cg.reflectList('applied').filter((p) => p.type === 'memory_rule');
1472
+ if (options.json) {
1473
+ // eslint-disable-next-line no-console
1474
+ console.log(JSON.stringify(items, null, 2));
1475
+ cg.destroy();
1476
+ return;
1477
+ }
1478
+ if (items.length === 0) {
1479
+ info('No memory items yet — capture one with `specship memory capture`.');
1480
+ cg.destroy();
1481
+ return;
1482
+ }
1483
+ for (const p of items) {
1484
+ const target = p.targetKind === 'claude_md' ? 'CLAUDE.md' : 'memory note';
1485
+ success(`${p.contentHash.slice(0, 12)} [${target}] ${p.title}`);
1486
+ }
1487
+ cg.destroy();
1488
+ }
1489
+ catch (err) {
1490
+ error(`memory list failed: ${err instanceof Error ? err.message : String(err)}`);
1491
+ process.exit(1);
1492
+ }
1493
+ });
1494
+ memory
1495
+ .command('remove <id>')
1496
+ .description('Remove a SpecShip-managed memory rule by id (previewed; pass --yes to write).')
1497
+ .option('--yes', 'actually remove it (without this it is a dry-run preview)')
1498
+ .option('--path <projectRoot>', 'project root (default: cwd)')
1499
+ .action(async (id, options) => {
1500
+ const projectPath = resolveProjectPath(options.path);
1501
+ try {
1502
+ if (!(0, directory_1.isInitialized)(projectPath)) {
1503
+ error(`SpecShip not initialized in ${projectPath}`);
1504
+ process.exit(1);
1505
+ }
1506
+ const { SpecShip } = await loadSpecShip();
1507
+ const cg = await SpecShip.open(projectPath);
1508
+ const p = cg.reflectGet(id) ??
1509
+ cg.reflectList().find((x) => x.type === 'memory_rule' && x.contentHash.startsWith(id));
1510
+ if (!p || p.type !== 'memory_rule') {
1511
+ error(`No SpecShip-managed memory rule found for "${id}".`);
1512
+ cg.destroy();
1513
+ process.exit(1);
1514
+ }
1515
+ info(`Will remove from ${p.targetPath}:`);
1516
+ // eslint-disable-next-line no-console
1517
+ console.log(memoryRuleBody(p));
1518
+ if (!options.yes) {
1519
+ info('Dry-run — nothing changed. Re-run with --yes to remove.');
1520
+ cg.destroy();
1521
+ return;
1522
+ }
1523
+ const outcome = cg.reflectUndo(p.contentHash);
1524
+ if (outcome === 'undone')
1525
+ success(`Removed ${p.contentHash.slice(0, 12)}: ${p.title}`);
1526
+ else
1527
+ info(`Nothing on disk to remove for ${p.contentHash.slice(0, 12)} (already gone).`);
1528
+ cg.destroy();
1529
+ }
1530
+ catch (err) {
1531
+ error(`memory remove failed: ${err instanceof Error ? err.message : String(err)}`);
1532
+ process.exit(1);
1533
+ }
1534
+ });
1535
+ memory
1536
+ .command('edit <id>')
1537
+ .description("Replace a memory rule's body with new content on stdin (previewed; pass --yes to write).")
1538
+ .option('--yes', 'actually apply the update (without this it is a dry-run before→after preview)')
1539
+ .option('--path <projectRoot>', 'project root (default: cwd)')
1540
+ .action(async (id, options) => {
1541
+ const projectPath = resolveProjectPath(options.path);
1542
+ try {
1543
+ if (!(0, directory_1.isInitialized)(projectPath)) {
1544
+ error(`SpecShip not initialized in ${projectPath}`);
1545
+ process.exit(1);
1546
+ }
1547
+ const chunks = [];
1548
+ for await (const c of process.stdin)
1549
+ chunks.push(c);
1550
+ const content = Buffer.concat(chunks).toString('utf-8').trim();
1551
+ if (!content) {
1552
+ error('memory edit expects the new body on stdin (pipe or heredoc).');
1553
+ process.exit(1);
1554
+ }
1555
+ const { SpecShip } = await loadSpecShip();
1556
+ const cg = await SpecShip.open(projectPath);
1557
+ const old = cg.reflectGet(id) ??
1558
+ cg.reflectList().find((x) => x.type === 'memory_rule' && x.contentHash.startsWith(id));
1559
+ if (!old || old.type !== 'memory_rule') {
1560
+ error(`No SpecShip-managed memory rule found for "${id}".`);
1561
+ cg.destroy();
1562
+ process.exit(1);
1563
+ }
1564
+ const title = old.title.replace(/^Lesson:\s*/, '');
1565
+ const target = old.targetKind === 'claude_md' ? 'claude-md' : 'memory';
1566
+ const next = cg.reflectCaptureLesson({ title, content, target });
1567
+ info(`Update ${old.contentHash.slice(0, 12)} — before → after:`);
1568
+ // eslint-disable-next-line no-console
1569
+ console.log('--- before ---\n' + memoryRuleBody(old) + '\n--- after ---\n' + memoryRuleBody(next));
1570
+ if (!options.yes) {
1571
+ info('Dry-run — nothing changed. Re-run with --yes to apply the update.');
1572
+ cg.destroy();
1573
+ return;
1574
+ }
1575
+ cg.reflectUndo(old.contentHash);
1576
+ const outcome = cg.reflectApply(next.contentHash);
1577
+ success(`Updated: removed ${old.contentHash.slice(0, 12)}, applied ${next.contentHash.slice(0, 12)} (${outcome}).`);
1578
+ cg.destroy();
1579
+ }
1580
+ catch (err) {
1581
+ error(`memory edit failed: ${err instanceof Error ? err.message : String(err)}`);
1582
+ process.exit(1);
1583
+ }
1584
+ });
1359
1585
  /**
1360
1586
  * specship maintainability
1361
1587
  *
@@ -2363,6 +2589,23 @@ function main() {
2363
2589
  // Index unreadable / FTS5 missing → print nothing (REQ-ACTIVATION-004.A1).
2364
2590
  }
2365
2591
  });
2592
+ /**
2593
+ * specship cheatsheet — print the user-facing session cheat-sheet
2594
+ * (CHEATSHEET-DOC). Installed as a startup-only `SessionStart` hook; emits a
2595
+ * `{ systemMessage }` payload the harness renders to the human. Silent when
2596
+ * `SPECSHIP_NO_CHEATSHEET` is set (REQ-CHEAT-004). Distinct from
2597
+ * `starter-prompt` (dynamic first-run flow) — this is the static capability map.
2598
+ */
2599
+ program
2600
+ .command('cheatsheet')
2601
+ .description('Print the SpecShip session cheat-sheet (used by the SessionStart hook).')
2602
+ .action(async () => {
2603
+ const { buildCheatsheetPayload } = await Promise.resolve().then(() => __importStar(require('../activation/cheatsheet')));
2604
+ const payload = buildCheatsheetPayload();
2605
+ if (payload)
2606
+ process.stdout.write(JSON.stringify(payload) + '\n');
2607
+ process.exit(0);
2608
+ });
2366
2609
  /**
2367
2610
  * specship spec-nudge (internal — installed as a UserPromptSubmit hook)
2368
2611
  *
@@ -3098,6 +3341,7 @@ function main() {
3098
3341
  .option('--deployment <kind>', 'Deployment kind: "cloud" or "datacenter" (inferred if omitted)')
3099
3342
  .option('--ca-cert <pem>', 'Path to a PEM CA bundle for a corporate/self-signed certificate (Data Center)')
3100
3343
  .option('--insecure-tls', 'Disable TLS certificate verification for JIRA requests only (last resort)')
3344
+ .option('--project <key>', 'Default project for spec→JIRA publishing (interactive setup offers your accessible projects)')
3101
3345
  .action(async (opts) => {
3102
3346
  const { saveJiraConfig, resolveJiraCredentials, inferDeployment, jiraConfigPath, JiraClient, JiraAuthError, } = await Promise.resolve().then(() => __importStar(require('../jira')));
3103
3347
  const clack = await importESM('@clack/prompts');
@@ -3197,7 +3441,40 @@ function main() {
3197
3441
  // Probe the connection so the user knows immediately whether it works.
3198
3442
  const client = new JiraClient(resolveJiraCredentials());
3199
3443
  const result = await client.testConnection();
3200
- clack.outro(`Connected as ${result.displayName ?? 'unknown user'}.`);
3444
+ clack.log.success(`Connected as ${result.displayName ?? 'unknown user'}.`);
3445
+ // Publish project (REQ-JIRAPUB-009.A3): an explicit --project saves
3446
+ // directly; otherwise the interactive path offers the projects this
3447
+ // account can actually browse, so the user picks instead of typing.
3448
+ if (opts.project) {
3449
+ config.project = opts.project.trim().toUpperCase();
3450
+ saveJiraConfig(config);
3451
+ clack.log.success(`Default publish project: ${config.project}.`);
3452
+ }
3453
+ else if (!flagged) {
3454
+ try {
3455
+ const projects = await client.listProjects();
3456
+ if (projects.length > 0) {
3457
+ const NONE = '__none__';
3458
+ const chosen = await clack.select({
3459
+ message: 'Default project for spec→JIRA publishing (specship_jira_publish)',
3460
+ options: [
3461
+ ...projects.map((p) => ({ value: p.key, label: `${p.key} — ${p.name}` })),
3462
+ { value: NONE, label: 'Skip — choose per publish' },
3463
+ ],
3464
+ });
3465
+ if (!cancelled(chosen) && chosen !== NONE) {
3466
+ config.project = String(chosen);
3467
+ saveJiraConfig(config);
3468
+ clack.log.success(`Default publish project: ${config.project}.`);
3469
+ }
3470
+ }
3471
+ }
3472
+ catch {
3473
+ // Listing projects is a convenience — a fault here never fails
3474
+ // an otherwise-successful configure.
3475
+ }
3476
+ }
3477
+ clack.outro('JIRA is configured.');
3201
3478
  }
3202
3479
  catch (err) {
3203
3480
  // Messages from the jira module are already scrubbed of the secret.
@@ -3219,6 +3496,24 @@ function main() {
3219
3496
  const kind = creds.deployment === 'cloud' ? 'JIRA Cloud' : 'JIRA Data Center';
3220
3497
  // Never print the credential — only the resolved identity.
3221
3498
  success(`Connected to ${kind} as ${result.displayName ?? 'unknown user'}.`);
3499
+ // Validate the configured lifecycle transition names against a live
3500
+ // workflow so a name the workflow can't fire is visible now, not a
3501
+ // silent skip at completion time (REQ-JIRATRANS-002).
3502
+ const { validateConfiguredTransitions } = await Promise.resolve().then(() => __importStar(require('../mcp/jira-tools')));
3503
+ const v = await validateConfiguredTransitions(client, creds.transitions ?? {});
3504
+ if (!v.verified) {
3505
+ info('Configured transitions: could not verify (no accessible issue to sample).');
3506
+ }
3507
+ else {
3508
+ info(`Configured transitions (checked against ${v.sampleKey}; available from its ` +
3509
+ `current state: ${v.available.join(', ') || 'none'}):`);
3510
+ for (const c of v.checks) {
3511
+ if (c.found)
3512
+ info(` ✓ ${c.role} "${c.configured}"`);
3513
+ else
3514
+ warn(` ✗ ${c.role} "${c.configured}" — not offered from ${v.sampleKey}'s current state`);
3515
+ }
3516
+ }
3222
3517
  }
3223
3518
  catch (err) {
3224
3519
  const msg = err instanceof Error ? err.message : String(err);
@@ -3227,6 +3522,41 @@ function main() {
3227
3522
  process.exit(1);
3228
3523
  }
3229
3524
  });
3525
+ /**
3526
+ * specship jira transition — move a tracked issue to a target state, or list
3527
+ * the transitions it currently offers (REQ-JIRATRANS-001). Reuses the client's
3528
+ * skip-tolerant transition: a target the workflow doesn't offer is reported
3529
+ * with the available states and nothing is written.
3530
+ */
3531
+ jira
3532
+ .command('transition <key> [state]')
3533
+ .description('Transition a JIRA issue to <state>, or list its available transitions when <state> is omitted (or with --list).')
3534
+ .option('--list', 'list the available transitions instead of applying one')
3535
+ .action(async (key, state, options) => {
3536
+ const { resolveJiraCredentials, JiraClient } = await Promise.resolve().then(() => __importStar(require('../jira')));
3537
+ try {
3538
+ const creds = resolveJiraCredentials();
3539
+ const client = new JiraClient(creds);
3540
+ if (options.list || !state) {
3541
+ const names = (await client.listTransitions(key)).map((t) => t.name);
3542
+ success(names.length
3543
+ ? `${key} can transition to: ${names.join(', ')}.`
3544
+ : `${key} has no available transitions from its current state.`);
3545
+ return;
3546
+ }
3547
+ const res = await client.transitionIssue(key, state);
3548
+ if ('transitioned' in res) {
3549
+ success(`Moved ${key} to "${res.transitioned}".`);
3550
+ }
3551
+ else {
3552
+ warn(`Did not transition ${key} — ${res.reason}.`);
3553
+ }
3554
+ }
3555
+ catch (err) {
3556
+ error(`JIRA transition failed: ${err instanceof Error ? err.message : String(err)}`);
3557
+ process.exit(1);
3558
+ }
3559
+ });
3230
3560
  /**
3231
3561
  * specship jira track — read-only tracking view (REQ-JIRA-008). Joins each
3232
3562
  * picked issue's SpecShip work-state (from its workflow run) with its LIVE JIRA
@@ -3247,7 +3577,7 @@ function main() {
3247
3577
  const { handleSpecshipJiraTrack } = await Promise.resolve().then(() => __importStar(require('../mcp/jira-tools')));
3248
3578
  const cg = await SpecShip.open(projectRoot);
3249
3579
  try {
3250
- const result = await handleSpecshipJiraTrack({ project: options.project }, { specQueries: cg.getSpecQueries() });
3580
+ const result = await handleSpecshipJiraTrack({ project: options.project }, { specQueries: cg.getSpecQueries(), projectRoot });
3251
3581
  const out = result.content.map((c) => c.text).join('\n');
3252
3582
  // eslint-disable-next-line no-console
3253
3583
  console.log(out);
@@ -3258,6 +3588,67 @@ function main() {
3258
3588
  cg.close();
3259
3589
  }
3260
3590
  });
3591
+ /**
3592
+ * specship jira release — stamp a released version onto issues
3593
+ * (REQ-JIRAPUB-007): ensure the project version exists, add it as fixVersion
3594
+ * on each issue, and add one shipped-in comment. Idempotent — a re-run
3595
+ * creates no duplicate version, fixVersion, or comment.
3596
+ */
3597
+ jira
3598
+ .command('release <version>')
3599
+ .description('Set <version> as fixVersion on JIRA issues (creating the project version if missing) with a shipped-in comment.')
3600
+ .option('--keys <keys>', 'comma-separated issue keys (e.g., "PROJ-1,PROJ-2"); default: every published spec\'s jira_issue key')
3601
+ .option('--project <key>', 'JIRA project key the version belongs to (default: the configured publish project)')
3602
+ .option('--path <projectRoot>', 'project root whose specs/ supplies the default keys (default: cwd)')
3603
+ .action(async (version, options) => {
3604
+ const { resolveJiraCredentials, JiraClient } = await Promise.resolve().then(() => __importStar(require('../jira')));
3605
+ const { releaseIssues } = await Promise.resolve().then(() => __importStar(require('../jira/publish')));
3606
+ const { readSpecJiraKey } = await Promise.resolve().then(() => __importStar(require('../jira/spec-writer')));
3607
+ try {
3608
+ const creds = resolveJiraCredentials();
3609
+ const projectKey = options.project ?? creds.project;
3610
+ if (!projectKey) {
3611
+ error('No JIRA project configured. Pass --project, set SPECSHIP_JIRA_PROJECT, or add "project" to your jira config.');
3612
+ process.exit(1);
3613
+ }
3614
+ let keys = (options.keys ?? '')
3615
+ .split(',')
3616
+ .map((k) => k.trim())
3617
+ .filter(Boolean);
3618
+ if (keys.length === 0) {
3619
+ // Default: every spec under <root>/specs with a jira_issue key.
3620
+ const projectRoot = path.resolve(options.path ?? process.cwd());
3621
+ const specsDir = path.join(projectRoot, 'specs');
3622
+ try {
3623
+ for (const name of fs.readdirSync(specsDir)) {
3624
+ if (!name.toLowerCase().endsWith('.md'))
3625
+ continue;
3626
+ const key = readSpecJiraKey(path.join(specsDir, name));
3627
+ if (key)
3628
+ keys.push(key);
3629
+ }
3630
+ }
3631
+ catch {
3632
+ /* no specs dir → empty keys, handled below */
3633
+ }
3634
+ keys = [...new Set(keys)];
3635
+ }
3636
+ if (keys.length === 0) {
3637
+ error('No issue keys to release — pass --keys or publish specs first (specship_jira_publish).');
3638
+ process.exit(1);
3639
+ }
3640
+ const client = new JiraClient(creds);
3641
+ const result = await releaseIssues(client, projectKey, version, keys);
3642
+ const stamped = result.issues.filter((i) => i.fixVersionAdded).length;
3643
+ const commented = result.issues.filter((i) => i.commented).length;
3644
+ success(`Version ${version}${result.versionCreated ? ' created' : ' already existed'} in ${projectKey}; ` +
3645
+ `fixVersion added to ${stamped}/${keys.length} issue(s), shipped-in comment added to ${commented}.`);
3646
+ }
3647
+ catch (err) {
3648
+ error(`JIRA release failed: ${err instanceof Error ? err.message : String(err)}`);
3649
+ process.exit(1);
3650
+ }
3651
+ });
3261
3652
  // Parse and run
3262
3653
  program.parse();
3263
3654
  } // end main()