@planu/cli 4.11.9 → 4.12.2

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 (91) hide show
  1. package/CHANGELOG.md +129 -22
  2. package/dist/engine/abortable-process-runner.d.ts +8 -0
  3. package/dist/engine/abortable-process-runner.js +107 -0
  4. package/dist/engine/cascade-hooks/core/append-releases.d.ts +1 -0
  5. package/dist/engine/cascade-hooks/core/append-releases.js +51 -14
  6. package/dist/engine/cascade-hooks/hooks/orchestration-plan.hook.js +16 -21
  7. package/dist/engine/ci-generator/yaml-builder.js +5 -1
  8. package/dist/engine/context-md-generator.js +3 -1
  9. package/dist/engine/criterion-identity.d.ts +6 -0
  10. package/dist/engine/criterion-identity.js +63 -0
  11. package/dist/engine/evidence-gates/lifecycle-gate.js +22 -44
  12. package/dist/engine/evidence-index/index-builder.js +7 -19
  13. package/dist/engine/frontmatter-parser.d.ts +5 -0
  14. package/dist/engine/frontmatter-parser.js +24 -0
  15. package/dist/engine/project-command-runtime.d.ts +5 -0
  16. package/dist/{tools/validate-runtime.js → engine/project-command-runtime.js} +8 -21
  17. package/dist/engine/project-graph/builder.js +1 -35
  18. package/dist/engine/project-graph/cache.js +37 -1
  19. package/dist/engine/qa-gate.d.ts +4 -1
  20. package/dist/engine/qa-gate.js +72 -59
  21. package/dist/engine/safety/cross-process-lock.d.ts +1 -6
  22. package/dist/engine/safety/cross-process-lock.js +18 -16
  23. package/dist/engine/safety/lock-scavenger.d.ts +9 -0
  24. package/dist/engine/safety/lock-scavenger.js +114 -0
  25. package/dist/engine/session-context-generator.js +24 -2
  26. package/dist/engine/session-safeguard/learnings-buffer.js +7 -2
  27. package/dist/engine/session-state/writer.js +15 -8
  28. package/dist/engine/skill-registry/installer.js +1 -1
  29. package/dist/engine/spec-format/acceptance-criteria.js +8 -26
  30. package/dist/engine/spec-grounding/contract.d.ts +3 -1
  31. package/dist/engine/spec-grounding/contract.js +39 -14
  32. package/dist/engine/universal-rules/installer.js +2 -2
  33. package/dist/engine/validation-evidence-digest.d.ts +4 -0
  34. package/dist/engine/validation-evidence-digest.js +83 -0
  35. package/dist/engine/validation-evidence-ledger.d.ts +30 -0
  36. package/dist/engine/validation-evidence-ledger.js +274 -0
  37. package/dist/engine/validation-impact-planner.d.ts +4 -0
  38. package/dist/engine/validation-impact-planner.js +71 -0
  39. package/dist/engine/validator/spec-compliance-runner.d.ts +1 -1
  40. package/dist/engine/validator/spec-compliance-runner.js +37 -40
  41. package/dist/engine/validator/validation-report-writer.d.ts +1 -0
  42. package/dist/engine/validator/validation-report-writer.js +1 -1
  43. package/dist/engine/validator.js +7 -11
  44. package/dist/storage/convention-baseline.js +5 -0
  45. package/dist/storage/session-state-store.js +13 -6
  46. package/dist/storage/spec-store.d.ts +5 -0
  47. package/dist/storage/spec-store.js +7 -0
  48. package/dist/tools/git/git-helpers.d.ts +5 -1
  49. package/dist/tools/git/git-helpers.js +3 -2
  50. package/dist/tools/init-project/conventions-writer.js +1 -1
  51. package/dist/tools/init-project/scaffold-writer.js +1 -1
  52. package/dist/tools/safe-handler.d.ts +5 -1
  53. package/dist/tools/safe-handler.js +73 -12
  54. package/dist/tools/status-handler.d.ts +1 -1
  55. package/dist/tools/status-handler.js +27 -66
  56. package/dist/tools/update-status/dod-gates.js +7 -31
  57. package/dist/tools/update-status/evidence-gate.js +2 -4
  58. package/dist/tools/update-status/force-done-audit.d.ts +2 -0
  59. package/dist/tools/update-status/force-done-audit.js +23 -0
  60. package/dist/tools/update-status/qa-gate.d.ts +1 -0
  61. package/dist/tools/update-status/qa-gate.js +7 -51
  62. package/dist/tools/update-status-convention-gate.js +132 -97
  63. package/dist/tools/validate-lint.d.ts +1 -1
  64. package/dist/tools/validate-lint.js +78 -165
  65. package/dist/tools/validate.js +3 -1
  66. package/dist/transports/http-transport.js +22 -8
  67. package/dist/transports/mcp-types.d.ts +2 -2
  68. package/dist/transports/transport-factory.js +110 -12
  69. package/dist/types/abortable-process.d.ts +26 -0
  70. package/dist/types/abortable-process.js +2 -0
  71. package/dist/types/criterion-identity.d.ts +12 -0
  72. package/dist/types/criterion-identity.js +2 -0
  73. package/dist/types/index.d.ts +2 -0
  74. package/dist/types/index.js +2 -0
  75. package/dist/types/qa-gate.d.ts +0 -5
  76. package/dist/types/spec-format.d.ts +2 -0
  77. package/dist/types/spec-lock-v2.d.ts +7 -0
  78. package/dist/types/validation-evidence.d.ts +62 -0
  79. package/dist/types/validation-evidence.js +2 -0
  80. package/package.json +43 -23
  81. package/planu-native.json +1 -1
  82. package/planu-plugin.json +1 -1
  83. package/dist/storage/qa-gate-store.d.ts +0 -4
  84. package/dist/storage/qa-gate-store.js +0 -24
  85. package/dist/tools/validate-runtime.d.ts +0 -14
  86. package/dist/types/data/estimation.d.ts +0 -147
  87. package/dist/types/data/estimation.js +0 -2
  88. package/dist/types/data/index.d.ts +0 -5
  89. package/dist/types/data/index.js +0 -6
  90. package/dist/types/data/velocity.d.ts +0 -168
  91. package/dist/types/data/velocity.js +0 -4
package/CHANGELOG.md CHANGED
@@ -1,3 +1,110 @@
1
+ ## [4.12.2] - 2026-07-27
2
+
3
+ ### Bug Fixes
4
+ - fix: restore dark language menu contrast
5
+
6
+
7
+ ## [4.12.1] - 2026-07-27
8
+
9
+ ### Bug Fixes
10
+ - fix: prevent personal metadata exposure (#62)
11
+
12
+
13
+ ## [4.12.0] - 2026-07-27
14
+
15
+ ### Features
16
+ - feat(website): add multichain donations (#61)
17
+
18
+
19
+ ## [4.11.20] - 2026-07-27
20
+
21
+ ### Bug Fixes
22
+ - fix(website): align desktop landing controls (#60)
23
+ - fix(release): bind reproducibility to annotated tags (#59)
24
+
25
+ ### Chores
26
+ - chore(planu): close SPEC-1155 lifecycle
27
+
28
+
29
+ ## [4.11.19] - 2026-07-27
30
+
31
+ ### Bug Fixes
32
+ - fix(website): repair responsive visual system (#58)
33
+ - fix(website): close production delivery gaps (#57)
34
+
35
+ ### Refactoring
36
+ - refactor(website): simplify landing around real evidence (#55)
37
+
38
+ ### Chores
39
+ - chore(planu): close SPEC-1153
40
+ - chore(planu): close SPEC-1151 lifecycle
41
+
42
+
43
+ ## [4.11.18] - 2026-07-26
44
+
45
+ ### Bug Fixes
46
+ - fix(security): remove exposed receipt from website
47
+
48
+
49
+ ## [4.11.17] - 2026-07-26
50
+
51
+ ### Bug Fixes
52
+ - fix: clear released specs from pending ledger
53
+
54
+
55
+ ## [4.11.16] - 2026-07-26
56
+
57
+ ### Bug Fixes
58
+ - fix: restore trustworthy maintenance audit gates
59
+
60
+
61
+ ## [4.11.15] - 2026-07-26
62
+
63
+ ### Bug Fixes
64
+ - fix: stabilize project graph freshness (#51)
65
+
66
+
67
+ ## [4.11.14] - 2026-07-25
68
+
69
+ ### Bug Fixes
70
+ - fix: parse canonical active spec status (#50)
71
+
72
+
73
+ ## [4.11.13] - 2026-07-25
74
+
75
+ ### Chores
76
+ - chore(deps): update ESLint to 10.8.0 (#49)
77
+
78
+
79
+ ## [4.11.12] - 2026-07-25
80
+
81
+ ### Bug Fixes
82
+ - fix(deps): remediate brace-expansion advisory
83
+
84
+ ### Chores
85
+ - chore(planu): close SPEC-1142 lifecycle
86
+ - chore(planu): close SPEC-1141 lifecycle
87
+
88
+
89
+ ## [4.11.11] - 2026-07-24
90
+
91
+ ### Bug Fixes
92
+ - fix: finalize release state reconciliation
93
+ - fix: preserve pnpm policy in Docker builds
94
+
95
+ ### Chores
96
+ - chore(deps): refresh routine dependencies
97
+
98
+
99
+ ## [4.11.10] - 2026-07-24
100
+
101
+ ### Bug Fixes
102
+ - fix: preserve verified release artifacts
103
+
104
+ ### Chores
105
+ - chore: harden release and state reconciliation
106
+
107
+
1
108
  ## [4.11.9] - 2026-07-20
2
109
 
3
110
  ### Bug Fixes
@@ -4124,54 +4231,54 @@ Total: 275 tools (72 free + 192 pro + 11 always-on)
4124
4231
 
4125
4232
  * add website tsconfig.json to avoid ES2024 esbuild error ([8184375](https://github.com/planu-dev/planu/commit/818437593775eac2ec56dd863f895c3390254783))
4126
4233
 
4127
- ## [0.15.1](https://github.com/ikevin92/specforge-mcp/compare/v0.15.0...v0.15.1) (2026-03-02)
4234
+ ## [0.15.1](https://github.com/imaintainer92/specforge-mcp/compare/v0.15.0...v0.15.1) (2026-03-02)
4128
4235
 
4129
4236
 
4130
4237
  ### Bug Fixes
4131
4238
 
4132
- * **npm:** publish under [@specforge-tools](https://github.com/specforge-tools) org to replace personal account ([adadff0](https://github.com/ikevin92/specforge-mcp/commit/adadff0dbeabd5b15386715a4c26e26cdfff1593))
4239
+ * **npm:** publish under [@specforge-tools](https://github.com/specforge-tools) org to replace personal account ([adadff0](https://github.com/imaintainer92/specforge-mcp/commit/adadff0dbeabd5b15386715a4c26e26cdfff1593))
4133
4240
 
4134
- # [0.15.0](https://github.com/ikevin92/specforge-mcp/compare/v0.14.1...v0.15.0) (2026-03-02)
4241
+ # [0.15.0](https://github.com/imaintainer92/specforge-mcp/compare/v0.14.1...v0.15.0) (2026-03-02)
4135
4242
 
4136
4243
 
4137
4244
  ### Features
4138
4245
 
4139
- * **licensing:** implement Lemon Squeezy license enforcement system ([bcd99d9](https://github.com/ikevin92/specforge-mcp/commit/bcd99d9c2331335874dccd5a8a08bae358a87486))
4246
+ * **licensing:** implement Lemon Squeezy license enforcement system ([bcd99d9](https://github.com/imaintainer92/specforge-mcp/commit/bcd99d9c2331335874dccd5a8a08bae358a87486))
4140
4247
 
4141
- ## [0.14.1](https://github.com/ikevin92/specforge-mcp/compare/v0.14.0...v0.14.1) (2026-03-02)
4248
+ ## [0.14.1](https://github.com/imaintainer92/specforge-mcp/compare/v0.14.0...v0.14.1) (2026-03-02)
4142
4249
 
4143
4250
 
4144
4251
  ### Bug Fixes
4145
4252
 
4146
- * **website:** disable empty aside sidebar on pricing pages ([440dfb2](https://github.com/ikevin92/specforge-mcp/commit/440dfb2dbcc60aa9393ea9954dfccb03d2515617))
4253
+ * **website:** disable empty aside sidebar on pricing pages ([440dfb2](https://github.com/imaintainer92/specforge-mcp/commit/440dfb2dbcc60aa9393ea9954dfccb03d2515617))
4147
4254
 
4148
- # [0.14.0](https://github.com/ikevin92/specforge-mcp/compare/v0.13.4...v0.14.0) (2026-03-02)
4255
+ # [0.14.0](https://github.com/imaintainer92/specforge-mcp/compare/v0.13.4...v0.14.0) (2026-03-02)
4149
4256
 
4150
4257
 
4151
4258
  ### Features
4152
4259
 
4153
- * **website:** redesign homepage sections, pricing cards, and fix broken links ([cc802d9](https://github.com/ikevin92/specforge-mcp/commit/cc802d911c499341b4f6c54b6d708bd2a0c891ec))
4260
+ * **website:** redesign homepage sections, pricing cards, and fix broken links ([cc802d9](https://github.com/imaintainer92/specforge-mcp/commit/cc802d911c499341b4f6c54b6d708bd2a0c891ec))
4154
4261
 
4155
- ## [0.13.4](https://github.com/ikevin92/specforge-mcp/compare/v0.13.3...v0.13.4) (2026-03-02)
4262
+ ## [0.13.4](https://github.com/imaintainer92/specforge-mcp/compare/v0.13.3...v0.13.4) (2026-03-02)
4156
4263
 
4157
4264
 
4158
4265
  ### Bug Fixes
4159
4266
 
4160
- * **website:** add vercel.json with cleanUrls for proper URL routing ([13878b5](https://github.com/ikevin92/specforge-mcp/commit/13878b563c64fa24f11a0e7feda034e2dec807c8))
4267
+ * **website:** add vercel.json with cleanUrls for proper URL routing ([13878b5](https://github.com/imaintainer92/specforge-mcp/commit/13878b563c64fa24f11a0e7feda034e2dec807c8))
4161
4268
 
4162
- ## [0.13.3](https://github.com/ikevin92/specforge-mcp/compare/v0.13.2...v0.13.3) (2026-03-02)
4269
+ ## [0.13.3](https://github.com/imaintainer92/specforge-mcp/compare/v0.13.2...v0.13.3) (2026-03-02)
4163
4270
 
4164
4271
 
4165
4272
  ### Bug Fixes
4166
4273
 
4167
- * **website:** enable cleanUrls to fix 11 empty pages on Vercel ([6714a52](https://github.com/ikevin92/specforge-mcp/commit/6714a5252cb358af7b2e95c980e873836efd4544))
4274
+ * **website:** enable cleanUrls to fix 11 empty pages on Vercel ([6714a52](https://github.com/imaintainer92/specforge-mcp/commit/6714a5252cb358af7b2e95c980e873836efd4544))
4168
4275
 
4169
- ## [0.13.2](https://github.com/ikevin92/specforge-mcp/compare/v0.13.1...v0.13.2) (2026-03-02)
4276
+ ## [0.13.2](https://github.com/imaintainer92/specforge-mcp/compare/v0.13.1...v0.13.2) (2026-03-02)
4170
4277
 
4171
4278
 
4172
4279
  ### Bug Fixes
4173
4280
 
4174
- * **ci:** add @semantic-release/changelog to auto-update CHANGELOG.md ([92834a3](https://github.com/ikevin92/specforge-mcp/commit/92834a32440df8e7b3c62c7b0ad03c29ed99089d))
4281
+ * **ci:** add @semantic-release/changelog to auto-update CHANGELOG.md ([92834a3](https://github.com/imaintainer92/specforge-mcp/commit/92834a32440df8e7b3c62c7b0ad03c29ed99089d))
4175
4282
 
4176
4283
  # Changelog
4177
4284
 
@@ -4192,30 +4299,30 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) · Versioning:
4192
4299
  - Duplicate `Architecture` row appearing outside the metadata table in FICHA-TECNICA.md
4193
4300
  - Structured multi-paragraph descriptions no longer wrapped in "As a user, I want..." in HU.md
4194
4301
 
4195
- ## [0.13.1](https://github.com/ikevin92/specforge-mcp/compare/v0.13.0...v0.13.1) — 2026-03-01
4302
+ ## [0.13.1](https://github.com/imaintainer92/specforge-mcp/compare/v0.13.0...v0.13.1) — 2026-03-01
4196
4303
 
4197
4304
  ### Fixed
4198
4305
  - **website:** Redesign PricingCards and clarify technical terms in copy
4199
4306
 
4200
- ## [0.13.0](https://github.com/ikevin92/specforge-mcp/compare/v0.12.1...v0.13.0) — 2026-03-01
4307
+ ## [0.13.0](https://github.com/imaintainer92/specforge-mcp/compare/v0.12.1...v0.13.0) — 2026-03-01
4201
4308
 
4202
4309
  ### Added
4203
4310
  - **website:** Replace donation system with pricing page and visual components
4204
4311
  - Auto-sync script for tool count across website and smoke test
4205
4312
  - Documentation updated to reflect 60 tools after context_budget addition
4206
4313
 
4207
- ## [0.12.1](https://github.com/ikevin92/specforge-mcp/compare/v0.12.0...v0.12.1) — 2026-02-28
4314
+ ## [0.12.1](https://github.com/imaintainer92/specforge-mcp/compare/v0.12.0...v0.12.1) — 2026-02-28
4208
4315
 
4209
4316
  ### Fixed
4210
4317
  - **tests:** Update smoke test tool count to 60 after context_budget addition
4211
4318
 
4212
- ## [0.12.0](https://github.com/ikevin92/specforge-mcp/compare/v0.11.1...v0.12.0) — 2026-02-27
4319
+ ## [0.12.0](https://github.com/imaintainer92/specforge-mcp/compare/v0.11.1...v0.12.0) — 2026-02-27
4213
4320
 
4214
4321
  ### Added
4215
4322
  - **tools:** `context_budget` tool for context window optimization (SPEC-063)
4216
4323
  - 60 MCP tools total
4217
4324
 
4218
- ## [0.11.1](https://github.com/ikevin92/specforge-mcp/compare/v0.11.0...v0.11.1) — 2026-02-27
4325
+ ## [0.11.1](https://github.com/imaintainer92/specforge-mcp/compare/v0.11.0...v0.11.1) — 2026-02-27
4219
4326
 
4220
4327
  ### Fixed
4221
4328
  - **ux:** Improve `summarize_spec` and `generate_docs` tool descriptions to prevent LLM bypass
@@ -4223,7 +4330,7 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) · Versioning:
4223
4330
  ### Changed
4224
4331
  - Refactor: split 5 files near 400L limit into subdirectories
4225
4332
 
4226
- ## [0.11.0](https://github.com/ikevin92/specforge-mcp/compare/v0.10.0...v0.11.0) — 2026-02-27
4333
+ ## [0.11.0](https://github.com/imaintainer92/specforge-mcp/compare/v0.10.0...v0.11.0) — 2026-02-27
4227
4334
 
4228
4335
  ### Added
4229
4336
  - **engine:** Executive summary HTML export (SPEC-060)
@@ -4231,13 +4338,13 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) · Versioning:
4231
4338
  - **engine:** Structured risk, complexity and trade-off documentation (SPEC-062)
4232
4339
  - 213 new tests across 54 files (+5,618 lines)
4233
4340
 
4234
- ## [0.10.0](https://github.com/ikevin92/specforge-mcp/compare/v0.9.0...v0.10.0) — 2026-02-27
4341
+ ## [0.10.0](https://github.com/imaintainer92/specforge-mcp/compare/v0.9.0...v0.10.0) — 2026-02-27
4235
4342
 
4236
4343
  ### Added
4237
4344
  - **ux:** Git branch suggestions in MCP workflow (e.g., `feat/SPEC-XXX-name` after creating specs)
4238
4345
  - **ux:** Git Flow enforcement — remind users to work in dedicated branches
4239
4346
 
4240
- ## [0.9.0](https://github.com/ikevin92/specforge-mcp/compare/v0.8.0...v0.9.0) — 2026-02-26
4347
+ ## [0.9.0](https://github.com/imaintainer92/specforge-mcp/compare/v0.8.0...v0.9.0) — 2026-02-26
4241
4348
 
4242
4349
  ### Added
4243
4350
  - **ux:** Global server instructions with plain-language glossary for all technical terms
@@ -0,0 +1,8 @@
1
+ import type { AbortableProcessOptions, AbortableProcessResult } from '../types/abortable-process.js';
2
+ /**
3
+ * Runs a validation command in its own process group. Cancellation and timeouts
4
+ * terminate the complete group and settle only after the child has closed, so a
5
+ * cancelled gate cannot keep writing output or publish late evidence.
6
+ */
7
+ export declare function runAbortableProcess(command: string, args: string[], options: AbortableProcessOptions): Promise<AbortableProcessResult>;
8
+ //# sourceMappingURL=abortable-process-runner.d.ts.map
@@ -0,0 +1,107 @@
1
+ import { execFileSync, spawn } from 'node:child_process';
2
+ /**
3
+ * Runs a validation command in its own process group. Cancellation and timeouts
4
+ * terminate the complete group and settle only after the child has closed, so a
5
+ * cancelled gate cannot keep writing output or publish late evidence.
6
+ */
7
+ export function runAbortableProcess(command, args, options) {
8
+ return new Promise((resolve) => {
9
+ if (options.signal?.aborted === true) {
10
+ resolve({
11
+ stdout: '',
12
+ stderr: '',
13
+ status: null,
14
+ signal: null,
15
+ error: abortError(options.signal.reason),
16
+ timedOut: false,
17
+ aborted: true,
18
+ });
19
+ return;
20
+ }
21
+ const child = spawn(command, args, {
22
+ cwd: options.cwd,
23
+ detached: process.platform !== 'win32',
24
+ stdio: ['ignore', 'pipe', 'pipe'],
25
+ });
26
+ let stdout = '';
27
+ let stderr = '';
28
+ let settled = false;
29
+ let timedOut = false;
30
+ let aborted = false;
31
+ let processError;
32
+ let hardStop;
33
+ const append = (current, chunk) => `${current}${chunk.toString('utf8')}`.slice(-options.maxBufferBytes);
34
+ child.stdout.on('data', (chunk) => {
35
+ stdout = append(stdout, chunk);
36
+ });
37
+ child.stderr.on('data', (chunk) => {
38
+ stderr = append(stderr, chunk);
39
+ });
40
+ const terminateTree = () => {
41
+ if (child.pid === undefined) {
42
+ return;
43
+ }
44
+ if (process.platform === 'win32') {
45
+ try {
46
+ execFileSync('taskkill', ['/pid', String(child.pid), '/T', '/F'], { stdio: 'ignore' });
47
+ }
48
+ catch {
49
+ child.kill('SIGKILL');
50
+ }
51
+ return;
52
+ }
53
+ try {
54
+ process.kill(-child.pid, 'SIGTERM');
55
+ }
56
+ catch {
57
+ child.kill('SIGTERM');
58
+ }
59
+ hardStop = setTimeout(() => {
60
+ try {
61
+ process.kill(-(child.pid ?? 0), 'SIGKILL');
62
+ }
63
+ catch {
64
+ child.kill('SIGKILL');
65
+ }
66
+ }, 250);
67
+ hardStop.unref();
68
+ };
69
+ const onAbort = () => {
70
+ aborted = true;
71
+ processError = abortError(options.signal?.reason);
72
+ terminateTree();
73
+ };
74
+ options.signal?.addEventListener('abort', onAbort, { once: true });
75
+ const timer = setTimeout(() => {
76
+ timedOut = true;
77
+ processError = new Error(`Process timed out after ${String(options.timeoutMs)}ms`);
78
+ terminateTree();
79
+ }, options.timeoutMs);
80
+ const finish = (status, closeSignal) => {
81
+ if (settled) {
82
+ return;
83
+ }
84
+ settled = true;
85
+ clearTimeout(timer);
86
+ clearTimeout(hardStop);
87
+ options.signal?.removeEventListener('abort', onAbort);
88
+ resolve({
89
+ stdout,
90
+ stderr,
91
+ status,
92
+ signal: closeSignal,
93
+ ...(processError ? { error: processError } : {}),
94
+ timedOut,
95
+ aborted,
96
+ });
97
+ };
98
+ child.on('error', (error) => {
99
+ processError = error;
100
+ });
101
+ child.on('close', finish);
102
+ });
103
+ }
104
+ function abortError(reason) {
105
+ return reason instanceof Error ? reason : new Error('Validation process aborted');
106
+ }
107
+ //# sourceMappingURL=abortable-process-runner.js.map
@@ -1,4 +1,5 @@
1
1
  import type { CascadeContext, CoreActionResult, PendingReleaseEntry } from '../../../types/cascade-hooks.js';
2
2
  export declare function normalizePendingReleaseEntries(value: unknown, now?: Date): PendingReleaseEntry[];
3
+ export declare function reconcilePublishedPendingEntries(entries: readonly PendingReleaseEntry[], latestPublishedAt: string | null): PendingReleaseEntry[];
3
4
  export declare function appendReleasesPending(ctx: CascadeContext): Promise<CoreActionResult>;
4
5
  //# sourceMappingURL=append-releases.d.ts.map
@@ -2,10 +2,14 @@
2
2
  // Appends spec to releases/pending.json within a 2-second AbortController budget.
3
3
  // Logic extracted verbatim from src/tools/update-status-actions.ts (SPEC-649/SPEC-660 block).
4
4
  import { join } from 'node:path';
5
- import { readFile, writeFile, mkdir } from 'node:fs/promises';
5
+ import { readFile, mkdir } from 'node:fs/promises';
6
+ import { execFile as execFileCallback } from 'node:child_process';
7
+ import { promisify } from 'node:util';
6
8
  import { specStore } from '../../../storage/index.js';
9
+ import { withPendingReleaseLock, writePendingReleaseLedger, } from '../../../../scripts/lib/pending-release-file.mjs';
7
10
  const CORE_ACTION_BUDGET_MS = 2000;
8
11
  const PENDING_ENTRY_MAX_AGE_DAYS = 30;
12
+ const execFile = promisify(execFileCallback);
9
13
  function isValidPendingReleaseEntry(value) {
10
14
  return (typeof value === 'object' &&
11
15
  value !== null &&
@@ -34,6 +38,35 @@ export function normalizePendingReleaseEntries(value, now = new Date()) {
34
38
  }
35
39
  return [...deduped.values()];
36
40
  }
41
+ export function reconcilePublishedPendingEntries(entries, latestPublishedAt) {
42
+ if (!latestPublishedAt || !/^\d{4}-\d{2}-\d{2}/.test(latestPublishedAt)) {
43
+ return [...entries];
44
+ }
45
+ const releaseDay = latestPublishedAt.slice(0, 10);
46
+ // Date-only completion metadata cannot prove ordering within the release day.
47
+ // Preserve same-day entries fail-closed; a later release can reconcile them safely.
48
+ return entries.filter((entry) => entry.completedAt.slice(0, 10) >= releaseDay);
49
+ }
50
+ async function readLatestPublishedReleaseDate(projectPath) {
51
+ try {
52
+ const manifest = JSON.parse(await readFile(join(projectPath, 'package.json'), 'utf-8'));
53
+ if (typeof manifest.version !== 'string' ||
54
+ !/^\d+\.\d+\.\d+(?:[-+].*)?$/.test(manifest.version)) {
55
+ return null;
56
+ }
57
+ const tag = `v${manifest.version}`;
58
+ const { stdout } = await execFile('git', ['show', '-s', '--format=%cI', tag], {
59
+ cwd: projectPath,
60
+ timeout: 1_000,
61
+ maxBuffer: 16 * 1024,
62
+ });
63
+ const publishedAt = stdout.trim();
64
+ return Number.isNaN(Date.parse(publishedAt)) ? null : publishedAt;
65
+ }
66
+ catch {
67
+ return null;
68
+ }
69
+ }
37
70
  async function actuallyAppendReleasesPending(ctx, opts) {
38
71
  const { projectPath, projectId, specId } = ctx;
39
72
  if (!projectPath || ctx.newStatus !== 'done') {
@@ -51,19 +84,23 @@ async function actuallyAppendReleasesPending(ctx, opts) {
51
84
  (async () => {
52
85
  await mkdir(releasesDir, { recursive: true });
53
86
  const spec = await specStore.getSpec(projectId, specId);
54
- let pendingList = [];
55
- try {
56
- const raw = await readFile(pendingPath, 'utf-8');
57
- const parsed = JSON.parse(raw);
58
- pendingList = normalizePendingReleaseEntries(parsed);
59
- }
60
- catch {
61
- /* file doesn't exist yet — start fresh */
62
- }
63
- const completedAt = new Date().toISOString().substring(0, 10);
64
- pendingList = pendingList.filter((entry) => entry.specId !== specId);
65
- pendingList.push({ specId, title: spec?.title ?? specId, completedAt });
66
- await writeFile(pendingPath, JSON.stringify(pendingList, null, 2), 'utf-8');
87
+ const latestPublishedAt = await readLatestPublishedReleaseDate(projectPath);
88
+ await withPendingReleaseLock(pendingPath, { timeoutMs: 1_500, signal: opts.signal }, async () => {
89
+ let pendingList = [];
90
+ try {
91
+ const raw = await readFile(pendingPath, 'utf-8');
92
+ const parsed = JSON.parse(raw);
93
+ pendingList = normalizePendingReleaseEntries(parsed);
94
+ }
95
+ catch {
96
+ /* file doesn't exist yet — start fresh */
97
+ }
98
+ pendingList = reconcilePublishedPendingEntries(pendingList, latestPublishedAt);
99
+ const completedAt = new Date().toISOString().substring(0, 10);
100
+ pendingList = pendingList.filter((entry) => entry.specId !== specId);
101
+ pendingList.push({ specId, title: spec?.title ?? specId, completedAt });
102
+ await writePendingReleaseLedger(pendingPath, pendingList);
103
+ });
67
104
  // Auto-commit planu/ changes so pending.json is never left unstaged
68
105
  void (async () => {
69
106
  try {
@@ -1,37 +1,32 @@
1
1
  async function handler(ctx) {
2
- const { projectPath, allSpecs, specId } = ctx;
2
+ const { projectPath, projectId } = ctx;
3
3
  if (!projectPath) {
4
4
  return;
5
5
  }
6
- const approvedCount = allSpecs.filter((s) => s.status === 'approved' || s.id === specId).length;
7
- if (approvedCount >= 2) {
8
- // Scenario 12: N≥2 approved → resolve orchestration plan
9
- try {
6
+ try {
7
+ const { specStore } = await import('../../../storage/index.js');
8
+ await specStore.withFreshSpecsLock(projectId, async (freshSpecs) => {
9
+ const approvedCount = freshSpecs.filter((spec) => spec.status === 'approved').length;
10
+ if (approvedCount < 2) {
11
+ const { writeOrchestrationPlan } = await import('../../../engine/next-spec-resolver/session-writer.js');
12
+ await writeOrchestrationPlan(projectPath, null);
13
+ return;
14
+ }
10
15
  const { resolveOrchestrationPlan } = await import('../../../engine/next-spec-resolver/orchestration-planner.js');
11
16
  const { writeOrchestrationPlan } = await import('../../../engine/next-spec-resolver/session-writer.js');
12
- const plan = await resolveOrchestrationPlan(projectPath, allSpecs);
17
+ const plan = await resolveOrchestrationPlan(projectPath, freshSpecs);
13
18
  await writeOrchestrationPlan(projectPath, plan);
14
- }
15
- catch {
16
- /* best-effort — never throw */
17
- }
19
+ });
18
20
  }
19
- else {
20
- // Scenario 15: < 2 approved → clear orchestration plan
21
- try {
22
- const { writeOrchestrationPlan } = await import('../../../engine/next-spec-resolver/session-writer.js');
23
- await writeOrchestrationPlan(projectPath, null);
24
- }
25
- catch {
26
- /* best-effort — never throw */
27
- }
21
+ catch {
22
+ /* best-effort never throw */
28
23
  }
29
24
  }
30
25
  export const orchestrationPlanHook = {
31
26
  id: 'orchestration-plan',
32
- triggers: ['approved'],
27
+ triggers: ['approved', 'done'],
33
28
  requiresProjectPath: true,
34
- label: 'Auto-generate/clear orchestration plan on approved (SPEC-686, Scenarios 12 & 15)',
29
+ label: 'Auto-generate/clear orchestration plan on approved or done',
35
30
  handler,
36
31
  };
37
32
  //# sourceMappingURL=orchestration-plan.hook.js.map
@@ -123,7 +123,11 @@ function buildNodeSetupSteps(ctx) {
123
123
  const version = ctx.nodeVersion ?? '20';
124
124
  const steps = [checkoutStep()];
125
125
  if (ctx.packageManager === 'pnpm') {
126
- steps.push({ name: 'Setup pnpm', uses: 'pnpm/action-setup@v4', with: { version: 'latest' } });
126
+ steps.push({
127
+ name: 'Setup pnpm',
128
+ uses: 'pnpm/action-setup@v4.4.0',
129
+ with: { version: 'latest' },
130
+ });
127
131
  }
128
132
  steps.push({
129
133
  name: 'Setup Node.js',
@@ -73,7 +73,9 @@ function formatBacklog(specs) {
73
73
  export async function generateContextMd(projectPath, projectId) {
74
74
  try {
75
75
  const specs = await specStore.listSpecs(projectId);
76
- const implementing = specs.filter((s) => s.status === 'implementing');
76
+ const implementing = specs
77
+ .filter((s) => s.status === 'implementing')
78
+ .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
77
79
  const backlog = specs
78
80
  .filter((s) => s.status === 'draft' || s.status === 'approved')
79
81
  .sort((a, b) => b.estimation.devHours - a.estimation.devHours);
@@ -0,0 +1,6 @@
1
+ import type { CriterionIdentity, CriterionIdentityOptions } from '../types/criterion-identity.js';
2
+ export type { CriterionIdentity, CriterionIdentityOptions } from '../types/criterion-identity.js';
3
+ export declare function normalizeCriterionText(value: string): string;
4
+ export declare function createCriterionIdentity(value: string, options?: CriterionIdentityOptions): CriterionIdentity;
5
+ export declare function criterionIdentitiesMatch(left: CriterionIdentity, right: CriterionIdentity): boolean;
6
+ //# sourceMappingURL=criterion-identity.d.ts.map
@@ -0,0 +1,63 @@
1
+ import { createHash } from 'node:crypto';
2
+ const CRITERION_ID_PREFIX = /^(AC|AB)\s*-?\s*(\d+)\b\s*[:.)-]?\s*/i;
3
+ export function normalizeCriterionText(value) {
4
+ return value
5
+ .normalize('NFKC')
6
+ .replace(/^[-*+]\s*\[[ xX]\]\s*/, '')
7
+ .replace(CRITERION_ID_PREFIX, '')
8
+ .replace(/[\p{P}]+/gu, ' ')
9
+ .replace(/[\s\u00a0]+/g, ' ')
10
+ .trim()
11
+ .toLowerCase();
12
+ }
13
+ export function createCriterionIdentity(value, options = {}) {
14
+ const stripped = value
15
+ .normalize('NFKC')
16
+ .replace(/^[-*+]\s*\[[ xX]\]\s*/, '')
17
+ .trim();
18
+ const explicit = CRITERION_ID_PREFIX.exec(stripped);
19
+ const fallbackOrdinal = (options.index ?? 0) + 1;
20
+ const kind = (explicit?.[1] ?? options.defaultKind ?? 'AC').toUpperCase();
21
+ const ordinal = String(Number.parseInt(explicit?.[2] ?? String(fallbackOrdinal), 10));
22
+ const id = `${kind}${ordinal}`;
23
+ const text = stripped.replace(CRITERION_ID_PREFIX, '').trim();
24
+ const normalizedText = normalizeCriterionText(text);
25
+ const aliases = new Set([
26
+ normalizeCriterionText(value),
27
+ normalizedText,
28
+ id.toLowerCase(),
29
+ `${kind.toLowerCase()}-${ordinal}`,
30
+ ]);
31
+ if (!explicit && options.index !== undefined) {
32
+ aliases.add(`ac${String(options.index + 1)}`);
33
+ aliases.add(`ac-${String(options.index + 1)}`);
34
+ aliases.add(`ab${String(options.index + 1)}`);
35
+ aliases.add(`ab-${String(options.index + 1)}`);
36
+ }
37
+ const stableKey = createHash('sha256')
38
+ .update(id.toLowerCase())
39
+ .update('\0')
40
+ .update(normalizedText)
41
+ .digest('hex')
42
+ .slice(0, 16);
43
+ return {
44
+ id,
45
+ text,
46
+ normalizedText,
47
+ stableKey,
48
+ aliases: [...aliases].filter(Boolean).sort(),
49
+ };
50
+ }
51
+ export function criterionIdentitiesMatch(left, right) {
52
+ if (left.stableKey === right.stableKey) {
53
+ return true;
54
+ }
55
+ if (left.normalizedText.length > 0 &&
56
+ right.normalizedText.length > 0 &&
57
+ left.normalizedText !== right.normalizedText) {
58
+ return false;
59
+ }
60
+ const rightAliases = new Set(right.aliases);
61
+ return left.aliases.some((alias) => rightAliases.has(alias));
62
+ }
63
+ //# sourceMappingURL=criterion-identity.js.map