@xulthekl/team-flow 0.31.0 → 0.32.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 (90) hide show
  1. package/.claude/always/phase-guard.md +1 -1
  2. package/.claude-plugin/marketplace.json +3 -3
  3. package/.claude-plugin/plugin.json +1 -1
  4. package/.codex-plugin/plugin.json +2 -2
  5. package/.cursor-plugin/marketplace.json +2 -2
  6. package/.cursor-plugin/plugin.json +2 -2
  7. package/.github/plugin/marketplace.json +2 -2
  8. package/AGENTS.md +3 -3
  9. package/CHANGELOG.md +69 -0
  10. package/GEMINI.md +1 -1
  11. package/HANDOFF.md +1 -1
  12. package/INSTALL.md +1 -1
  13. package/README.md +2 -2
  14. package/agents/contract-builder.md +6 -1
  15. package/docs/README_en.md +1 -1
  16. package/docs/solutions/INDEX.md +2 -0
  17. package/docs/solutions/cross-phase/2026-08-01-no-summary.md +17 -0
  18. package/docs/solutions/cross-phase/2026-08-03-no-summary.md +17 -0
  19. package/gemini-extension.json +2 -2
  20. package/hooks/pre-tool-use-guard +43 -2
  21. package/hooks/session-start +2 -2
  22. package/llms.txt +1 -1
  23. package/package.json +6 -2
  24. package/plugin.json +1 -1
  25. package/scripts/guard/checks/test-gate-exemptions.mjs +53 -0
  26. package/scripts/guard/checks/test-matrix-complete.mjs +54 -30
  27. package/scripts/guard/checks/test-matrix-ready.mjs +60 -0
  28. package/scripts/guard/checks/tests-passing.mjs +112 -24
  29. package/scripts/guard/guard.mjs +11 -2
  30. package/scripts/lib/cmd-doctor.mjs +43 -1
  31. package/scripts/lib/cmd-execution.mjs +24 -1
  32. package/scripts/lib/cmd-state.mjs +28 -5
  33. package/scripts/lib/cmd-validate.mjs +42 -0
  34. package/scripts/lib/cmd-version.mjs +26 -21
  35. package/scripts/lib/execution-plan.mjs +23 -0
  36. package/scripts/lib/state-loader.mjs +16 -2
  37. package/scripts/lib/test-record.mjs +212 -0
  38. package/scripts/team-flow.mjs +3 -0
  39. package/skills/contract-builder/SKILL.md +9 -10
  40. package/skills/release-archivist/SKILL.md +16 -9
  41. package/skills/session-handoff/SKILL.md +1 -0
  42. package/skills/workflow-start/SKILL.md +5 -3
  43. package/skills/workflow-start/references/routing-rules.md +10 -6
  44. package/tests/e2e.test.mjs +0 -266
  45. package/tests/lib/cmd-audit.test.mjs +0 -202
  46. package/tests/lib/cmd-checkpoint.test.mjs +0 -98
  47. package/tests/lib/cmd-config.test.mjs +0 -101
  48. package/tests/lib/cmd-doctor.test.mjs +0 -450
  49. package/tests/lib/cmd-execution.test.mjs +0 -647
  50. package/tests/lib/cmd-handoff.test.mjs +0 -137
  51. package/tests/lib/cmd-inject.test.mjs +0 -218
  52. package/tests/lib/cmd-install-workbuddy.test.mjs +0 -113
  53. package/tests/lib/cmd-install-zcode.test.mjs +0 -42
  54. package/tests/lib/cmd-isolate.test.mjs +0 -68
  55. package/tests/lib/cmd-list.test.mjs +0 -127
  56. package/tests/lib/cmd-runtime.test.mjs +0 -83
  57. package/tests/lib/cmd-state-missing.test.mjs +0 -65
  58. package/tests/lib/cmd-state.test.mjs +0 -365
  59. package/tests/lib/cmd-sync-paths.test.mjs +0 -84
  60. package/tests/lib/cmd-validate-paths.test.mjs +0 -90
  61. package/tests/lib/config-loader.test.mjs +0 -175
  62. package/tests/lib/ensure-branch.test.mjs +0 -110
  63. package/tests/lib/execution-control-plane.test.mjs +0 -168
  64. package/tests/lib/execution-plan.test.mjs +0 -474
  65. package/tests/lib/guard-compound-captured.test.mjs +0 -92
  66. package/tests/lib/guard-specs-merged.test.mjs +0 -110
  67. package/tests/lib/guard-tests-passing.test.mjs +0 -114
  68. package/tests/lib/guard-transitions.test.mjs +0 -262
  69. package/tests/lib/guard.test.mjs +0 -613
  70. package/tests/lib/hash.test.mjs +0 -138
  71. package/tests/lib/infer-workflow.test.mjs +0 -161
  72. package/tests/lib/install-git-hooks.test.mjs +0 -20
  73. package/tests/lib/marketplace-release-docs.test.mjs +0 -31
  74. package/tests/lib/minimality-discipline.test.mjs +0 -37
  75. package/tests/lib/model-profiles-docs.test.mjs +0 -33
  76. package/tests/lib/node20-compatibility.test.mjs +0 -73
  77. package/tests/lib/node20-test-entry.test.mjs +0 -17
  78. package/tests/lib/platform-runtime-distribution.test.mjs +0 -157
  79. package/tests/lib/raw-mode-smoke.test.mjs +0 -42
  80. package/tests/lib/sdd-overlay.test.mjs +0 -118
  81. package/tests/lib/solutions-capture.test.mjs +0 -108
  82. package/tests/lib/solutions-index-gen.test.mjs +0 -147
  83. package/tests/lib/solutions-inject.test.mjs +0 -115
  84. package/tests/lib/solutions-promote.test.mjs +0 -200
  85. package/tests/lib/spec-paths.test.mjs +0 -80
  86. package/tests/lib/state-loader.test.mjs +0 -251
  87. package/tests/lib/token-baseline.test.mjs +0 -72
  88. package/tests/lib/token-rules.test.mjs +0 -94
  89. package/tests/lib/verify-marketplace-release.test.mjs +0 -128
  90. package/tests/tsconfig.json +0 -12
@@ -1,474 +0,0 @@
1
- import { afterEach, beforeEach, describe, it } from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import { execFileSync } from 'node:child_process';
4
- import { lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
5
- import { join } from 'node:path';
6
- import { tmpdir } from 'node:os';
7
- import {
8
- createPlan as createRawPlan, readPlan, recordReview, validatePlan, writePlan,
9
- } from '../../scripts/lib/execution-plan.mjs';
10
- import { createRecommendationReceipt, recommendExecutionModes } from '../../scripts/lib/execution-recommendation.mjs';
11
- import { readState } from '../../scripts/lib/state-loader.mjs';
12
-
13
- let changeDir;
14
- let gitRefs;
15
-
16
- beforeEach(() => {
17
- changeDir = mkdtempSync(join(tmpdir(), 'execution-plan-'));
18
- writeFileSync(join(changeDir, 'tasks.md'), '# Tasks\n\n- [ ] 1.1 First task\n- [ ] 1.2 Second task\n');
19
- writeFileSync(join(changeDir, 'execution-contract.md'), '# Execution Contract\n\nCurrent contract.\n');
20
- writeFileSync(join(changeDir, '.team-flow.yaml'), 'state: approved-for-build\nworkflow: full\nrevision: 2\n');
21
- gitRefs = initializeGitRepository(changeDir);
22
- });
23
-
24
- afterEach(() => {
25
- rmSync(changeDir, { recursive: true, force: true });
26
- });
27
-
28
- function writeReviewReport(name, content = 'Review completed without blocking findings.\n') {
29
- const reportsDir = join(changeDir, '.superpowers', 'sdd', 'reviews');
30
- mkdirSync(reportsDir, { recursive: true });
31
- const reportPath = join(reportsDir, name);
32
- writeFileSync(reportPath, content);
33
- return reportPath;
34
- }
35
-
36
- function runGit(directory, args) {
37
- return execFileSync('git', args, { cwd: directory, encoding: 'utf8' }).trim();
38
- }
39
-
40
- function initializeGitRepository(directory) {
41
- runGit(directory, ['init', '--quiet']);
42
- runGit(directory, ['config', 'user.email', 'tests@example.invalid']);
43
- runGit(directory, ['config', 'user.name', 'Execution Plan Test']);
44
- runGit(directory, ['add', '--all']);
45
- runGit(directory, ['commit', '--quiet', '--message', 'initial execution plan change']);
46
- const base = runGit(directory, ['rev-parse', 'HEAD']);
47
- writeFileSync(join(directory, 'git-range-marker.txt'), 'second commit\n');
48
- runGit(directory, ['add', 'git-range-marker.txt']);
49
- runGit(directory, ['commit', '--quiet', '--message', 'second execution plan change']);
50
- return { base, head: runGit(directory, ['rev-parse', 'HEAD']) };
51
- }
52
-
53
- function createPlan(directory, input) {
54
- const receipt = input.recommendationReceipt ?? createRecommendationReceipt(directory, input.waves);
55
- const recommendation = input.recommendation ?? receipt.recommendation;
56
- const followedRecommendation = input.mode === recommendation.recommendation.mode;
57
- return createRawPlan(directory, {
58
- ...input,
59
- source: input.source === 'default' ? 'user-confirmed' : input.source,
60
- recommendation,
61
- recommendationReceipt: receipt,
62
- selection: input.selection ?? {
63
- confirmed: true,
64
- followed_recommendation: followedRecommendation,
65
- acknowledged_non_recommendation: !followedRecommendation,
66
- },
67
- });
68
- }
69
-
70
- describe('execution plan data contract', () => {
71
- it('recommends inline for one small sequential task', () => {
72
- const result = recommendExecutionModes({
73
- workflow: 'full',
74
- taskCount: 1,
75
- inlineThreshold: 3,
76
- waves: [{ id: 'wave-1', strategy: 'serial', tasks: ['1.1'], depends_on: [] }],
77
- });
78
-
79
- assert.deepEqual(result.available_modes, ['inline', 'batch-inline', 'sdd']);
80
- assert.equal(result.recommendation.mode, 'inline');
81
- assert.match(result.recommendation.reasons.join('\n'), /single sequential task/i);
82
- });
83
-
84
- it('recommends batch-inline for a bounded sequential batch', () => {
85
- const result = recommendExecutionModes({
86
- workflow: 'hotfix',
87
- taskCount: 3,
88
- inlineThreshold: 3,
89
- waves: [{ id: 'wave-1', strategy: 'serial', tasks: ['1.1', '1.2', '1.3'], depends_on: [] }],
90
- });
91
-
92
- assert.equal(result.recommendation.mode, 'batch-inline');
93
- assert.match(result.recommendation.reasons.join('\n'), /within.*threshold/i);
94
- });
95
-
96
- it('recommends SDD for independent parallel work', () => {
97
- const result = recommendExecutionModes({
98
- workflow: 'full',
99
- taskCount: 2,
100
- inlineThreshold: 3,
101
- waves: [{ id: 'foundation', strategy: 'parallel', tasks: ['1.1', '1.2'], depends_on: [] }],
102
- });
103
-
104
- assert.equal(result.recommendation.mode, 'sdd');
105
- assert.match(result.recommendation.reasons.join('\n'), /parallel/i);
106
- });
107
-
108
- it('limits tweak recommendations to direct inline execution', () => {
109
- const result = recommendExecutionModes({ workflow: 'tweak', taskCount: 2, inlineThreshold: 3, waves: [] });
110
-
111
- assert.deepEqual(result.available_modes, ['inline']);
112
- assert.equal(result.recommendation.mode, 'inline');
113
- });
114
-
115
- it('creates a current SDD plan with an auditable parallel wave', () => {
116
- const plan = createPlan(changeDir, {
117
- mode: 'sdd',
118
- source: 'default',
119
- rationale: 'full workflow default',
120
- waves: [{ id: 'wave-1', strategy: 'parallel', tasks: ['1.1', '1.2'], depends_on: [] }],
121
- });
122
-
123
- writePlan(changeDir, plan);
124
- const result = validatePlan(changeDir, readPlan(changeDir));
125
-
126
- assert.equal(result.valid, true, result.failures.join('\n'));
127
- assert.equal(result.plan.mode, 'sdd');
128
- assert.equal(readState(changeDir).execution_plan_hash, plan.hash);
129
- });
130
-
131
- it('preserves a user-confirmed non-recommended selection in the plan hash', () => {
132
- const plan = createPlan(changeDir, {
133
- mode: 'inline',
134
- source: 'user-confirmed',
135
- rationale: 'operator accepts the serial execution risk',
136
- waves: [{ id: 'wave-1', strategy: 'serial', tasks: ['1.1', '1.2'], depends_on: [] }],
137
- recommendation: {
138
- available_modes: ['inline', 'batch-inline', 'sdd'],
139
- recommendation: { mode: 'batch-inline', reasons: ['Two tasks are within the threshold.'] },
140
- facts: { workflow: 'full' },
141
- },
142
- selection: {
143
- confirmed: true,
144
- followed_recommendation: false,
145
- acknowledged_non_recommendation: true,
146
- },
147
- });
148
-
149
- assert.equal(plan.selection.acknowledged_non_recommendation, true);
150
- assert.equal(plan.recommendation.recommendation.mode, 'batch-inline');
151
- });
152
-
153
- it('invalidates a legacy full plan without recommendation and confirmation evidence', () => {
154
- const plan = createRawPlan(changeDir, {
155
- mode: 'sdd',
156
- source: 'legacy',
157
- rationale: 'legacy execution plan',
158
- waves: [{ id: 'wave-1', strategy: 'parallel', tasks: ['1.1', '1.2'], depends_on: [] }],
159
- });
160
- writePlan(changeDir, plan);
161
-
162
- const result = validatePlan(changeDir, readPlan(changeDir));
163
-
164
- assert.equal(result.valid, false);
165
- assert.match(result.failures.join('\n'), /recommendation.*required|selection.*required/i);
166
- });
167
-
168
- it('invalidates a revision whose recommendation skips the immediately prior plan', () => {
169
- const first = createPlan(changeDir, {
170
- mode: 'sdd',
171
- source: 'user-confirmed',
172
- rationale: 'initial execution plan',
173
- waves: [{ id: 'wave-1', strategy: 'parallel', tasks: ['1.1', '1.2'], depends_on: [] }],
174
- });
175
- writePlan(changeDir, first);
176
- writeFileSync(join(changeDir, '.team-flow.yaml'), readFileSync(join(changeDir, '.team-flow.yaml'), 'utf8')
177
- .replace(/^revision:.*$/m, 'revision: 4'));
178
- const receipt = createRecommendationReceipt(changeDir, first.waves);
179
- const revised = createRawPlan(changeDir, {
180
- mode: 'sdd',
181
- source: 'user-confirmed-revision',
182
- rationale: 'skips an execution plan revision',
183
- waves: first.waves,
184
- revision: 4,
185
- recommendation: receipt.recommendation,
186
- recommendationReceipt: receipt,
187
- selection: {
188
- confirmed: true,
189
- followed_recommendation: true,
190
- acknowledged_non_recommendation: false,
191
- },
192
- });
193
- writePlan(changeDir, revised);
194
-
195
- const result = validatePlan(changeDir, readPlan(changeDir));
196
-
197
- assert.equal(result.valid, false);
198
- assert.match(result.failures.join('\n'), /recommendation receipt.*prior|recommendation.*revision/i);
199
- });
200
-
201
- it('rejects parallel waves with self and unknown dependencies', () => {
202
- assert.throws(() => createPlan(changeDir, {
203
- mode: 'sdd',
204
- source: 'default',
205
- rationale: 'invalid dependencies',
206
- waves: [{ id: 'wave-1', strategy: 'parallel', tasks: ['1.1'], depends_on: ['wave-1'] }],
207
- }), /cannot depend on itself/);
208
-
209
- assert.throws(() => createPlan(changeDir, {
210
- mode: 'sdd',
211
- source: 'default',
212
- rationale: 'invalid dependencies',
213
- waves: [{ id: 'wave-1', strategy: 'parallel', tasks: ['1.1'], depends_on: ['missing'] }],
214
- }), /unknown wave/);
215
- });
216
-
217
- it('rejects dependency cycles and duplicate task IDs in any wave', () => {
218
- assert.throws(() => createPlan(changeDir, {
219
- mode: 'sdd',
220
- source: 'default',
221
- rationale: 'cyclic dependencies',
222
- waves: [
223
- { id: 'wave-1', strategy: 'serial', tasks: ['1.1'], depends_on: ['wave-2'] },
224
- { id: 'wave-2', strategy: 'serial', tasks: ['1.2'], depends_on: ['wave-1'] },
225
- ],
226
- }), /dependency cycle/);
227
-
228
- assert.throws(() => createPlan(changeDir, {
229
- mode: 'sdd',
230
- source: 'default',
231
- rationale: 'duplicate parallel task',
232
- waves: [{ id: 'wave-1', strategy: 'parallel', tasks: ['1.1', '1.1'], depends_on: [] }],
233
- }), /duplicate tasks/);
234
-
235
- assert.throws(() => createPlan(changeDir, {
236
- mode: 'sdd',
237
- source: 'default',
238
- rationale: 'duplicate task across waves',
239
- waves: [
240
- { id: 'wave-1', strategy: 'serial', tasks: ['1.1'], depends_on: [] },
241
- { id: 'wave-2', strategy: 'parallel', tasks: ['1.1', '1.2'], depends_on: [] },
242
- ],
243
- }), /duplicate task.*1\.1/i);
244
- });
245
-
246
- it('changes the plan hash when plan content changes', () => {
247
- const first = createPlan(changeDir, {
248
- mode: 'sdd', source: 'default', rationale: 'first rationale',
249
- waves: [{ id: 'wave-1', strategy: 'serial', tasks: ['1.1'], depends_on: [] }],
250
- });
251
- const second = createPlan(changeDir, {
252
- mode: 'sdd', source: 'default', rationale: 'second rationale',
253
- waves: [{ id: 'wave-1', strategy: 'serial', tasks: ['1.1'], depends_on: [] }],
254
- });
255
-
256
- assert.notEqual(first.hash, second.hash);
257
- });
258
-
259
- it('marks a plan stale after its frozen artifacts change', () => {
260
- const plan = createPlan(changeDir, {
261
- mode: 'sdd', source: 'default', rationale: 'freeze current artifacts',
262
- waves: [{ id: 'wave-1', strategy: 'serial', tasks: ['1.1'], depends_on: [] }],
263
- });
264
- writePlan(changeDir, plan);
265
- writeFileSync(join(changeDir, 'tasks.md'), '# Tasks\n\n- [ ] 1.1 Changed task\n');
266
-
267
- const result = validatePlan(changeDir, readPlan(changeDir));
268
-
269
- assert.equal(result.valid, false);
270
- assert.ok(result.failures.includes('execution plan is stale: artifacts hash mismatch'));
271
- });
272
-
273
- it('marks a plan stale after its frozen contract changes', () => {
274
- const plan = createPlan(changeDir, {
275
- mode: 'sdd', source: 'default', rationale: 'freeze current contract',
276
- waves: [{ id: 'wave-1', strategy: 'serial', tasks: ['1.1'], depends_on: [] }],
277
- });
278
- writePlan(changeDir, plan);
279
- writeFileSync(join(changeDir, 'execution-contract.md'), '# Execution Contract\n\nChanged contract.\n');
280
-
281
- const result = validatePlan(changeDir, readPlan(changeDir));
282
-
283
- assert.equal(result.valid, false);
284
- assert.ok(result.failures.includes('execution plan is stale: contract hash mismatch'));
285
- });
286
-
287
- it('marks a plan stale when the state revision changes', () => {
288
- const plan = createPlan(changeDir, {
289
- mode: 'sdd', source: 'default', rationale: 'freeze current revision',
290
- waves: [{ id: 'wave-1', strategy: 'serial', tasks: ['1.1'], depends_on: [] }],
291
- });
292
- writePlan(changeDir, plan);
293
- writeFileSync(join(changeDir, '.team-flow.yaml'), [
294
- 'state: approved-for-build',
295
- 'workflow: full',
296
- 'revision: 3',
297
- `execution_plan_hash: ${plan.hash}`,
298
- '',
299
- ].join('\n'));
300
-
301
- const result = validatePlan(changeDir, readPlan(changeDir));
302
-
303
- assert.equal(result.valid, false);
304
- assert.ok(result.failures.includes('execution plan revision does not match state'));
305
- });
306
-
307
- it('rejects a persisted plan when its state plan revision is deleted or differs', () => {
308
- const plan = createPlan(changeDir, {
309
- mode: 'sdd', source: 'default', rationale: 'freeze persisted plan revision',
310
- waves: [{ id: 'wave-1', strategy: 'serial', tasks: ['1.1'], depends_on: [] }],
311
- });
312
- writePlan(changeDir, plan);
313
-
314
- const stateVariants = [
315
- [
316
- 'state: approved-for-build',
317
- 'workflow: full',
318
- 'revision: 2',
319
- `execution_plan_hash: ${plan.hash}`,
320
- ],
321
- [
322
- 'state: approved-for-build',
323
- 'workflow: full',
324
- 'revision: 2',
325
- `execution_plan_hash: ${plan.hash}`,
326
- 'execution_plan_revision: null',
327
- ],
328
- [
329
- 'state: approved-for-build',
330
- 'workflow: full',
331
- 'revision: 2',
332
- `execution_plan_hash: ${plan.hash}`,
333
- 'execution_plan_revision:',
334
- ],
335
- [
336
- 'state: approved-for-build',
337
- 'workflow: full',
338
- 'revision: 2',
339
- `execution_plan_hash: ${plan.hash}`,
340
- 'execution_plan_revision: 3',
341
- ],
342
- ];
343
-
344
- for (const state of stateVariants) {
345
- writeFileSync(join(changeDir, '.team-flow.yaml'), `${state.join('\n')}\n`);
346
- const result = validatePlan(changeDir, readPlan(changeDir));
347
- assert.equal(result.valid, false);
348
- assert.ok(result.failures.includes('execution plan revision does not match state'));
349
- }
350
- });
351
-
352
- it('records review receipts only for known waves', () => {
353
- const plan = createPlan(changeDir, {
354
- mode: 'sdd', source: 'default', rationale: 'review gate',
355
- waves: [{ id: 'wave-1', strategy: 'parallel', tasks: ['1.1', '1.2'], depends_on: [] }],
356
- });
357
- writePlan(changeDir, plan);
358
-
359
- const reportPath = writeReviewReport('wave-1.md');
360
- const receipt = recordReview(changeDir, 'wave-1', {
361
- status: 'pass', base: gitRefs.base, head: gitRefs.head, report: reportPath,
362
- });
363
-
364
- assert.equal(receipt.status, 'pass');
365
- assert.ok(receipt.recorded_at);
366
- const reviewsDir = join(changeDir, '.superpowers', 'sdd', 'reviews');
367
- const receiptFiles = readdirSync(reviewsDir).filter(fileName => fileName.endsWith('.json'));
368
- assert.equal(receiptFiles.length, 1);
369
- assert.deepEqual(
370
- JSON.parse(readFileSync(join(reviewsDir, receiptFiles[0]), 'utf8')),
371
- receipt,
372
- );
373
- assert.throws(
374
- () => recordReview(changeDir, 'unknown-wave', {
375
- status: 'pass', base: gitRefs.base, head: gitRefs.head, report: reportPath,
376
- }),
377
- /unknown wave/,
378
- );
379
- });
380
-
381
- it('persists receipts independently for wave IDs with encoded-name collisions', () => {
382
- const plan = createPlan(changeDir, {
383
- mode: 'sdd', source: 'default', rationale: 'review receipt naming',
384
- waves: [
385
- { id: 'a%', strategy: 'serial', tasks: ['1.1'], depends_on: [] },
386
- { id: 'a_25', strategy: 'serial', tasks: ['1.2'], depends_on: ['a%'] },
387
- ],
388
- });
389
- writePlan(changeDir, plan);
390
-
391
- const percentReport = writeReviewReport('percent.md', 'Percent wave passed.\n');
392
- const underscoreReport = writeReviewReport('underscore.md', 'Underscore wave failed.\n');
393
- recordReview(changeDir, 'a%', {
394
- status: 'pass', base: gitRefs.base, head: gitRefs.head, report: percentReport,
395
- });
396
- recordReview(changeDir, 'a_25', {
397
- status: 'fail', base: gitRefs.base, head: gitRefs.head, report: underscoreReport,
398
- });
399
-
400
- const reviewsDir = join(changeDir, '.superpowers', 'sdd', 'reviews');
401
- const receipts = readdirSync(reviewsDir)
402
- .filter(fileName => fileName.endsWith('.json'))
403
- .sort()
404
- .map(fileName => JSON.parse(readFileSync(join(reviewsDir, fileName), 'utf8')));
405
- assert.equal(receipts.length, 2);
406
- assert.ok(receipts.some(receipt => receipt.report === join('.superpowers', 'sdd', 'reviews', 'percent.md')));
407
- assert.ok(receipts.some(receipt => receipt.report === join('.superpowers', 'sdd', 'reviews', 'underscore.md')));
408
- });
409
-
410
- it('rejects missing, non-file, empty, and symbolic-link report evidence before writing a receipt', () => {
411
- const plan = createPlan(changeDir, {
412
- mode: 'sdd', source: 'default', rationale: 'review evidence must be durable',
413
- waves: [{ id: 'wave-1', strategy: 'serial', tasks: ['1.1'], depends_on: [] }],
414
- });
415
- writePlan(changeDir, plan);
416
-
417
- const reportsDir = join(changeDir, '.superpowers', 'sdd', 'reviews');
418
- mkdirSync(reportsDir, { recursive: true });
419
- const emptyReport = join(reportsDir, 'empty.md');
420
- const directoryReport = join(reportsDir, 'directory');
421
- const validReport = writeReviewReport('valid.md');
422
- const symlinkReport = join(reportsDir, 'symlink.md');
423
- writeFileSync(emptyReport, '');
424
- mkdirSync(directoryReport);
425
- symlinkSync(validReport, symlinkReport);
426
-
427
- for (const report of [
428
- join(reportsDir, 'missing.md'),
429
- directoryReport,
430
- emptyReport,
431
- symlinkReport,
432
- ]) {
433
- assert.throws(() => recordReview(changeDir, 'wave-1', {
434
- status: 'pass', base: gitRefs.base, head: gitRefs.head, report,
435
- }), /report evidence|review report/i);
436
- assert.equal(lstatSync(join(reportsDir, 'valid.md')).isFile(), true);
437
- assert.equal(readdirSync(reportsDir).filter(fileName => fileName.endsWith('.json')).length, 0);
438
- }
439
- });
440
-
441
- it('preserves a legitimate review report path in the receipt', () => {
442
- const plan = createPlan(changeDir, {
443
- mode: 'sdd', source: 'default', rationale: 'retain report path for audit',
444
- waves: [{ id: 'wave-1', strategy: 'serial', tasks: ['1.1'], depends_on: [] }],
445
- });
446
- writePlan(changeDir, plan);
447
- const reportPath = writeReviewReport('audit.md');
448
-
449
- const receipt = recordReview(changeDir, 'wave-1', {
450
- status: 'pass', base: gitRefs.base, head: gitRefs.head, report: reportPath,
451
- });
452
-
453
- assert.equal(receipt.report, join('.superpowers', 'sdd', 'reviews', 'audit.md'));
454
- });
455
-
456
- it('returns validation failures instead of throwing for malformed plans', () => {
457
- const result = validatePlan(changeDir, { mode: 'sdd', waves: 'not-an-array' });
458
-
459
- assert.equal(result.valid, false);
460
- assert.ok(result.failures.length > 0);
461
- });
462
-
463
- it('returns validation failures for malformed dependency data', () => {
464
- const result = validatePlan(changeDir, {
465
- mode: 'sdd',
466
- source: 'default',
467
- rationale: 'malformed dependency input',
468
- waves: [{ id: 'wave-1', strategy: 'serial', tasks: ['1.1'], depends_on: {} }],
469
- });
470
-
471
- assert.equal(result.valid, false);
472
- assert.ok(result.failures.includes('wave 1 depends_on must be an array'));
473
- });
474
- });
@@ -1,92 +0,0 @@
1
- // tests/lib/guard-compound-captured.test.mjs
2
- // v0.24.0: compound-captured guard — executing:closing must require learnings.md
3
- // or explicit compound_skipped=true in state.
4
-
5
- import { describe, it, before, after } from 'node:test';
6
- import assert from 'node:assert/strict';
7
- import { mkdtempSync, writeFileSync, rmSync, mkdirSync, existsSync } from 'node:fs';
8
- import { join } from 'node:path';
9
- import { tmpdir } from 'node:os';
10
- import { fileURLToPath } from 'node:url';
11
- import { dirname } from 'node:path';
12
-
13
- const __dirname = dirname(fileURLToPath(import.meta.url));
14
- const ROOT = join(__dirname, '..', '..');
15
-
16
- // Import the check function directly for unit-level testing.
17
- const { checkCompoundCaptured } = await import(
18
- join(ROOT, 'scripts', 'guard', 'checks', 'compound-captured.mjs')
19
- );
20
-
21
- function makeDir(prefix) {
22
- return mkdtempSync(join(tmpdir(), `tf-compound-${prefix}-`));
23
- }
24
-
25
- function cleanup(dir) {
26
- if (existsSync(dir)) rmSync(dir, { recursive: true, force: true });
27
- }
28
-
29
- describe('compound-captured guard', () => {
30
- describe('learnings.md existence', () => {
31
- let dir;
32
- before(() => { dir = makeDir('learnings'); });
33
- after(() => cleanup(dir));
34
-
35
- it('SHALL fail when no learnings.md and no compound_skipped', () => {
36
- writeFileSync(join(dir, '.team-flow.yaml'), 'state: executing\nworkflow: full\n');
37
- const result = checkCompoundCaptured(dir);
38
- assert.equal(result.pass, false);
39
- assert.ok(result.failures.length > 0);
40
- assert.match(result.failures[0], /Compound capture missing/);
41
- });
42
-
43
- it('SHALL pass when learnings.md exists and is non-empty', () => {
44
- writeFileSync(join(dir, 'learnings.md'), '# Session Learnings\n\n## Lesson 1\nTest.\n');
45
- const result = checkCompoundCaptured(dir);
46
- assert.equal(result.pass, true);
47
- });
48
-
49
- it('SHALL fail when learnings.md exists but is empty', () => {
50
- writeFileSync(join(dir, 'learnings.md'), '');
51
- const result = checkCompoundCaptured(dir);
52
- assert.equal(result.pass, false);
53
- assert.match(result.failures[0], /empty/);
54
- });
55
- });
56
-
57
- describe('compound_skipped explicit opt-out', () => {
58
- let dir;
59
- before(() => { dir = makeDir('skip'); });
60
- after(() => cleanup(dir));
61
-
62
- it('SHALL pass when compound_skipped=true and no learnings.md', () => {
63
- writeFileSync(join(dir, '.team-flow.yaml'), 'state: executing\nworkflow: full\ncompound_skipped: true\n');
64
- const result = checkCompoundCaptured(dir);
65
- assert.equal(result.pass, true);
66
- });
67
-
68
- it('SHALL fail when compound_skipped=false (not opted out)', () => {
69
- writeFileSync(join(dir, '.team-flow.yaml'), 'state: executing\nworkflow: full\ncompound_skipped: false\n');
70
- const result = checkCompoundCaptured(dir);
71
- assert.equal(result.pass, false);
72
- });
73
- });
74
-
75
- describe('no state file (backward compatibility)', () => {
76
- let dir;
77
- before(() => { dir = makeDir('no-state'); });
78
- after(() => cleanup(dir));
79
-
80
- it('SHALL fail when no state file and no learnings.md', () => {
81
- // No .team-flow.yaml, no learnings.md — guard should still fail
82
- const result = checkCompoundCaptured(dir);
83
- assert.equal(result.pass, false);
84
- });
85
-
86
- it('SHALL pass when no state file but learnings.md exists', () => {
87
- writeFileSync(join(dir, 'learnings.md'), '# Learnings\nContent.\n');
88
- const result = checkCompoundCaptured(dir);
89
- assert.equal(result.pass, true);
90
- });
91
- });
92
- });
@@ -1,110 +0,0 @@
1
- // tests/lib/guard-specs-merged.test.mjs
2
- // Regression for #28: `executing → closing` must be blocked while delta specs
3
- // exist but spec-merger has not run (spec_merged not recorded).
4
-
5
- import { describe, it, before, after } from 'node:test';
6
- import assert from 'node:assert/strict';
7
- import { execFileSync } from 'node:child_process';
8
- import { mkdtempSync, writeFileSync, rmSync, mkdirSync, existsSync } from 'node:fs';
9
- import { join } from 'node:path';
10
- import { tmpdir } from 'node:os';
11
- import { fileURLToPath } from 'node:url';
12
- import { dirname } from 'node:path';
13
-
14
- const __dirname = dirname(fileURLToPath(import.meta.url));
15
- const ROOT = join(__dirname, '..', '..');
16
- const GUARD = join(ROOT, 'scripts', 'guard', 'guard.mjs');
17
- const CLI = join(ROOT, 'scripts', 'team-flow.mjs');
18
-
19
- function makeChangeDir(withDelta) {
20
- const dir = mkdtempSync(join(tmpdir(), 'tf-specs-merged-'));
21
- mkdirSync(join(dir, 'specs'), { recursive: true });
22
- writeFileSync(join(dir, 'proposal.md'), '# Test\n\n## Why\nTest.\n\n## What Changes\n- Test.\n');
23
- writeFileSync(join(dir, 'design.md'), '# Design\n\n## Context\nTest.\n\n## Goals\nTest.\n\n## Decisions\n\n### Decision 1\n- Choice: Test\n- Rationale: Test\n\n## Risks And Trade-Offs\nNone.\n');
24
- writeFileSync(join(dir, 'tasks.md'), '# Tasks\n\n- [x] Task 1\n- [x] Task 2\n');
25
- writeFileSync(join(dir, 'execution-contract.md'), '# Execution Contract\n\n## Intent Lock\nTest.\n');
26
- const specsContent = withDelta
27
- ? '## ADDED Requirements\n\n### Requirement: New\n\nThe system SHALL do new.\n\n#### Scenario: New\n- **WHEN** x\n- **THEN** y\n'
28
- : '## Requirements\n\n### Requirement: Existing\n\nThe system SHALL exist.\n\n#### Scenario: Existing\n- **WHEN** a\n- **THEN** b\n';
29
- writeFileSync(join(dir, 'specs', 'test.md'), specsContent);
30
- // v0.24.0: compound-captured dimension requires learnings.md
31
- writeFileSync(join(dir, 'learnings.md'), '# Session Learnings\n\n## Lesson 1\nTest.\n');
32
- initializeGitRepository(dir);
33
- return dir;
34
- }
35
-
36
- function runGit(directory, args) {
37
- return execFileSync('git', args, { cwd: directory, encoding: 'utf8' }).trim();
38
- }
39
-
40
- function initializeGitRepository(directory) {
41
- runGit(directory, ['init', '--quiet']);
42
- runGit(directory, ['config', 'user.email', 'tests@example.invalid']);
43
- runGit(directory, ['config', 'user.name', 'Guard Specs Merged Test']);
44
- runGit(directory, ['add', '--all']);
45
- runGit(directory, ['commit', '--quiet', '--message', 'initial closing guard change']);
46
- const base = runGit(directory, ['rev-parse', 'HEAD']);
47
- writeFileSync(join(directory, 'git-range-marker.txt'), 'second commit\n');
48
- runGit(directory, ['add', 'git-range-marker.txt']);
49
- runGit(directory, ['commit', '--quiet', '--message', 'second closing guard change']);
50
- return { base, head: runGit(directory, ['rev-parse', 'HEAD']) };
51
- }
52
-
53
- function cleanup(dir) {
54
- if (existsSync(dir)) rmSync(dir, { recursive: true, force: true });
55
- }
56
-
57
- function runClosingGuard(dir, extraState = '') {
58
- try {
59
- rmSync(join(dir, '.superpowers'), { recursive: true, force: true });
60
- writeFileSync(
61
- join(dir, '.team-flow.yaml'),
62
- `state: executing\nworkflow: full\nchange_name: test\ndp_6_result: pass: ok\n${extraState}`,
63
- );
64
- execFileSync('node', [CLI, 'execution', 'recommend', dir,
65
- '--wave', 'close:serial:1.1'], { stdio: 'pipe', timeout: 5000 });
66
- execFileSync('node', [CLI, 'execution', 'plan', dir, '--mode', 'sdd',
67
- '--confirm', '--acknowledge-recommendation', '--reason', 'closing guard regression fixture',
68
- '--wave', 'close:serial:1.1'], { stdio: 'pipe', timeout: 5000 });
69
- const report = join(dir, '.superpowers', 'sdd', 'reviews', 'close-review.md');
70
- mkdirSync(join(dir, '.superpowers', 'sdd', 'reviews'), { recursive: true });
71
- writeFileSync(report, 'review passed\n');
72
- const base = execFileSync('git', ['rev-parse', 'HEAD~1'], { cwd: dir, encoding: 'utf8' }).trim();
73
- const head = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: dir, encoding: 'utf8' }).trim();
74
- execFileSync('node', [CLI, 'execution', 'review', dir, '--wave', 'close',
75
- '--base', base, '--head', head, '--report', report, '--verdict', 'pass'], { stdio: 'pipe', timeout: 5000 });
76
- execFileSync('node', [GUARD, 'check', dir, 'executing', 'closing', '--json'], { stdio: 'pipe', timeout: 5000 });
77
- return { ok: true, out: '' };
78
- } catch (e) {
79
- const out = `${e.stdout?.toString() || ''}\n${e.stderr?.toString() || ''}`;
80
- return { ok: false, out: out || e.message };
81
- }
82
- }
83
-
84
- describe('BUG/#28: spec-merger gate before closing', () => {
85
- let noDeltaDir, deltaDir;
86
- before(() => {
87
- noDeltaDir = makeChangeDir(false);
88
- deltaDir = makeChangeDir(true);
89
- });
90
- after(() => {
91
- cleanup(noDeltaDir);
92
- cleanup(deltaDir);
93
- });
94
-
95
- it('SHALL allow closing when there are no delta specs', () => {
96
- const r = runClosingGuard(noDeltaDir);
97
- assert.equal(r.ok, true, `closing should be allowed without delta specs, got: ${r.out}`);
98
- });
99
-
100
- it('SHALL block closing when delta specs exist but spec-merger has not run', () => {
101
- const r = runClosingGuard(deltaDir);
102
- assert.equal(r.ok, false, 'closing must be blocked until delta specs are merged');
103
- assert.match(r.out, /spec-merger|spec_merged|delta specs/i);
104
- });
105
-
106
- it('SHALL allow closing when spec_merged is recorded', () => {
107
- const r = runClosingGuard(deltaDir, 'spec_merged: true\n');
108
- assert.equal(r.ok, true, `closing should be allowed once spec_merged=true, got: ${r.out}`);
109
- });
110
- });