@xulthekl/team-flow 0.30.0 → 0.32.1

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 (101) 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 +2 -2
  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 +5 -4
  9. package/CHANGELOG.md +85 -0
  10. package/GEMINI.md +1 -1
  11. package/HANDOFF.md +1 -1
  12. package/INSTALL.md +1 -1
  13. package/README.md +3 -3
  14. package/agents/build-executor.md +1 -0
  15. package/agents/contract-builder.md +6 -1
  16. package/docs/README_en.md +1 -1
  17. package/docs/solutions/INDEX.md +2 -0
  18. package/docs/solutions/cross-phase/2026-08-01-no-summary.md +17 -0
  19. package/docs/solutions/cross-phase/2026-08-03-no-summary.md +17 -0
  20. package/gemini-extension.json +2 -2
  21. package/hooks/session-start +2 -2
  22. package/llms.txt +1 -1
  23. package/package.json +6 -2
  24. package/plugin.json +2 -2
  25. package/scripts/guard/checks/test-gate-exemptions.mjs +53 -0
  26. package/scripts/guard/checks/test-matrix-complete.mjs +91 -0
  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 +10 -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 +33 -5
  33. package/scripts/lib/cmd-validate.mjs +42 -0
  34. package/scripts/lib/execution-plan.mjs +23 -0
  35. package/scripts/lib/hash.mjs +11 -0
  36. package/scripts/lib/state-loader.mjs +26 -1
  37. package/scripts/lib/test-matrix-export.mjs +231 -0
  38. package/scripts/lib/test-merge.mjs +540 -0
  39. package/scripts/lib/test-record.mjs +212 -0
  40. package/scripts/team-flow.mjs +9 -0
  41. package/skills/build-executor/implementer-prompt.md +38 -3
  42. package/skills/code-reviewer/SKILL.md +28 -1
  43. package/skills/code-reviewer/code-reviewer-prompt.md +10 -0
  44. package/skills/contract-builder/SKILL.md +75 -0
  45. package/skills/release-archivist/SKILL.md +50 -10
  46. package/skills/session-handoff/SKILL.md +1 -0
  47. package/skills/spec-writer/SKILL.md +3 -1
  48. package/skills/test-strategy/SKILL.md +70 -0
  49. package/skills/test-strategy/references/adversarial-patterns.md +0 -0
  50. package/skills/test-strategy/references/complexity-grading.md +137 -0
  51. package/skills/test-strategy/references/design-methods-detail.md +183 -0
  52. package/skills/workflow-orchestrator/references/s1-path-router.md +4 -0
  53. package/skills/workflow-start/SKILL.md +5 -3
  54. package/skills/workflow-start/references/routing-rules.md +21 -0
  55. package/tests/e2e.test.mjs +0 -266
  56. package/tests/lib/cmd-audit.test.mjs +0 -202
  57. package/tests/lib/cmd-checkpoint.test.mjs +0 -98
  58. package/tests/lib/cmd-config.test.mjs +0 -101
  59. package/tests/lib/cmd-doctor.test.mjs +0 -450
  60. package/tests/lib/cmd-execution.test.mjs +0 -647
  61. package/tests/lib/cmd-handoff.test.mjs +0 -137
  62. package/tests/lib/cmd-inject.test.mjs +0 -218
  63. package/tests/lib/cmd-install-workbuddy.test.mjs +0 -113
  64. package/tests/lib/cmd-install-zcode.test.mjs +0 -42
  65. package/tests/lib/cmd-isolate.test.mjs +0 -68
  66. package/tests/lib/cmd-list.test.mjs +0 -127
  67. package/tests/lib/cmd-runtime.test.mjs +0 -83
  68. package/tests/lib/cmd-state-missing.test.mjs +0 -65
  69. package/tests/lib/cmd-state.test.mjs +0 -365
  70. package/tests/lib/cmd-sync-paths.test.mjs +0 -84
  71. package/tests/lib/cmd-validate-paths.test.mjs +0 -90
  72. package/tests/lib/config-loader.test.mjs +0 -175
  73. package/tests/lib/ensure-branch.test.mjs +0 -110
  74. package/tests/lib/execution-control-plane.test.mjs +0 -168
  75. package/tests/lib/execution-plan.test.mjs +0 -474
  76. package/tests/lib/guard-compound-captured.test.mjs +0 -92
  77. package/tests/lib/guard-specs-merged.test.mjs +0 -110
  78. package/tests/lib/guard-tests-passing.test.mjs +0 -114
  79. package/tests/lib/guard-transitions.test.mjs +0 -262
  80. package/tests/lib/guard.test.mjs +0 -613
  81. package/tests/lib/hash.test.mjs +0 -138
  82. package/tests/lib/infer-workflow.test.mjs +0 -161
  83. package/tests/lib/install-git-hooks.test.mjs +0 -20
  84. package/tests/lib/marketplace-release-docs.test.mjs +0 -31
  85. package/tests/lib/minimality-discipline.test.mjs +0 -37
  86. package/tests/lib/model-profiles-docs.test.mjs +0 -33
  87. package/tests/lib/node20-compatibility.test.mjs +0 -73
  88. package/tests/lib/node20-test-entry.test.mjs +0 -17
  89. package/tests/lib/platform-runtime-distribution.test.mjs +0 -157
  90. package/tests/lib/raw-mode-smoke.test.mjs +0 -42
  91. package/tests/lib/sdd-overlay.test.mjs +0 -118
  92. package/tests/lib/solutions-capture.test.mjs +0 -108
  93. package/tests/lib/solutions-index-gen.test.mjs +0 -147
  94. package/tests/lib/solutions-inject.test.mjs +0 -115
  95. package/tests/lib/solutions-promote.test.mjs +0 -200
  96. package/tests/lib/spec-paths.test.mjs +0 -80
  97. package/tests/lib/state-loader.test.mjs +0 -251
  98. package/tests/lib/token-baseline.test.mjs +0 -72
  99. package/tests/lib/token-rules.test.mjs +0 -94
  100. package/tests/lib/verify-marketplace-release.test.mjs +0 -128
  101. package/tests/tsconfig.json +0 -12
@@ -1,115 +0,0 @@
1
- // tests/lib/solutions-inject.test.mjs
2
- // Tests for scripts/lib/solutions-inject.mjs — compound injection mechanism
3
-
4
- import { describe, it, before, after, beforeEach } from 'node:test';
5
- import assert from 'node:assert/strict';
6
- import { mkdtempSync, writeFileSync, existsSync, rmSync, mkdirSync } from 'node:fs';
7
- import { join } from 'node:path';
8
- import { tmpdir } from 'node:os';
9
- import { fileURLToPath } from 'node:url';
10
- import { dirname } from 'node:path';
11
-
12
- const __dirname = dirname(fileURLToPath(import.meta.url));
13
- const ROOT = join(__dirname, '..', '..');
14
-
15
- const { run: injectRun } = await import(join(ROOT, 'scripts', 'lib', 'solutions-inject.mjs'));
16
-
17
- function makeDir() {
18
- return mkdtempSync(join(tmpdir(), 'tf-sol-inject-'));
19
- }
20
-
21
- function cleanup(dir) {
22
- if (existsSync(dir)) rmSync(dir, { recursive: true, force: true });
23
- }
24
-
25
- const INDEX_HEADER = `# Solutions Index
26
- <!-- 每条一行,按 severity 降序,≤150 行硬上限 -->
27
- | date | phase | domain | type | severity | summary | file |
28
- |------|-------|--------|------|----------|---------|------|
29
- `;
30
-
31
- function writeIndex(dir, rows) {
32
- mkdirSync(dir, { recursive: true });
33
- writeFileSync(join(dir, 'INDEX.md'), INDEX_HEADER + rows.join('\n') + '\n', 'utf-8');
34
- }
35
-
36
- describe('solutions-inject', () => {
37
- let dir;
38
-
39
- beforeEach(() => { dir = makeDir(); });
40
- after(() => { cleanup(dir); });
41
-
42
- it('returns empty when no INDEX.md exists', () => {
43
- const result = injectRun({ phase: 'prd', dir });
44
- assert.deepEqual(result.entries, []);
45
- });
46
-
47
- it('filters by exact phase match', () => {
48
- writeIndex(dir, [
49
- '| 2026-07-01 | prd | auth | pitfall | high | PRD auth issue | prd/f1.md |',
50
- '| 2026-07-01 | build | auth | pitfall | high | Build auth issue | build/f2.md |',
51
- ]);
52
- const result = injectRun({ phase: 'prd', dir });
53
- assert.equal(result.entries.length, 1);
54
- assert.equal(result.entries[0].summary, 'PRD auth issue');
55
- });
56
-
57
- it('includes cross-phase entries for any phase query', () => {
58
- writeIndex(dir, [
59
- '| 2026-07-01 | cross-phase | general | pattern | high | Global pattern | cross-phase/f1.md |',
60
- '| 2026-07-01 | prd | auth | pitfall | high | PRD issue | prd/f2.md |',
61
- ]);
62
- const result = injectRun({ phase: 'build', dir });
63
- // cross-phase matches for any phase
64
- assert.equal(result.entries.length, 1);
65
- assert.equal(result.entries[0].phase, 'cross-phase');
66
- });
67
-
68
- it('filters by domain (exact match)', () => {
69
- writeIndex(dir, [
70
- '| 2026-07-01 | prd | auth | pitfall | high | Auth issue | prd/f1.md |',
71
- '| 2026-07-01 | prd | billing | pitfall | high | Billing issue | prd/f2.md |',
72
- ]);
73
- const result = injectRun({ phase: 'prd', domain: 'auth', dir });
74
- assert.equal(result.entries.length, 1);
75
- assert.equal(result.entries[0].domain, 'auth');
76
- });
77
-
78
- it('domain "general" matches any query', () => {
79
- writeIndex(dir, [
80
- '| 2026-07-01 | prd | general | pattern | high | General pattern | prd/f1.md |',
81
- ]);
82
- const result = injectRun({ phase: 'prd', domain: 'auth', dir });
83
- assert.equal(result.entries.length, 1);
84
- });
85
-
86
- it('sorts by severity descending (high > medium > low)', () => {
87
- writeIndex(dir, [
88
- '| 2026-07-01 | prd | auth | pitfall | low | Low issue | prd/f1.md |',
89
- '| 2026-07-01 | prd | auth | pitfall | high | High issue | prd/f2.md |',
90
- '| 2026-07-01 | prd | auth | pitfall | medium | Medium issue | prd/f3.md |',
91
- ]);
92
- const result = injectRun({ phase: 'prd', dir });
93
- assert.equal(result.entries[0].severity, 'high');
94
- assert.equal(result.entries[1].severity, 'medium');
95
- assert.equal(result.entries[2].severity, 'low');
96
- });
97
-
98
- it('limits to top-5 entries', () => {
99
- const rows = [];
100
- for (let i = 0; i < 10; i++) {
101
- rows.push(`| 2026-07-0${(i % 9) + 1} | prd | auth | pitfall | high | Issue ${i} | prd/f${i}.md |`);
102
- }
103
- writeIndex(dir, rows);
104
- const result = injectRun({ phase: 'prd', dir });
105
- assert.equal(result.entries.length, 5, 'should cap at top-5');
106
- });
107
-
108
- it('returns empty for no matching entries', () => {
109
- writeIndex(dir, [
110
- '| 2026-07-01 | build | ci | pattern | high | Build issue | build/f1.md |',
111
- ]);
112
- const result = injectRun({ phase: 'prd', dir });
113
- assert.deepEqual(result.entries, []);
114
- });
115
- });
@@ -1,200 +0,0 @@
1
- // tests/lib/solutions-promote.test.mjs
2
- // Tests for scripts/lib/solutions-promote.mjs — change→product promotion
3
-
4
- import { describe, it, before, after, beforeEach } from 'node:test';
5
- import assert from 'node:assert/strict';
6
- import { mkdtempSync, writeFileSync, existsSync, rmSync, mkdirSync, readFileSync } from 'node:fs';
7
- import { join } from 'node:path';
8
- import { tmpdir } from 'node:os';
9
- import { fileURLToPath } from 'node:url';
10
- import { dirname } from 'node:path';
11
-
12
- const __dirname = dirname(fileURLToPath(import.meta.url));
13
- const ROOT = join(__dirname, '..', '..');
14
-
15
- // NODE_ENV=test is set below before import — promote's CLI guard skips when test.
16
- process.env.NODE_ENV = 'test';
17
- const { run: promoteRun } = await import(join(ROOT, 'scripts', 'lib', 'solutions-promote.mjs'));
18
-
19
- function makeDir(prefix = '') {
20
- return mkdtempSync(join(tmpdir(), `tf-sol-promote-${prefix}-`));
21
- }
22
-
23
- function cleanup(dir) {
24
- if (existsSync(dir)) rmSync(dir, { recursive: true, force: true });
25
- }
26
-
27
- function writeLearnings(dir, content) {
28
- writeFileSync(join(dir, 'learnings.md'), content, 'utf-8');
29
- }
30
-
31
- const INDEX_HEADER = `# Solutions Index
32
- | date | phase | domain | type | severity | summary | file |
33
- |------|-------|--------|------|----------|---------|------|
34
- `;
35
-
36
- function writeGlobalIndex(solDir, rows = []) {
37
- mkdirSync(solDir, { recursive: true });
38
- writeFileSync(join(solDir, 'INDEX.md'), INDEX_HEADER + rows.join('\n') + '\n', 'utf-8');
39
- }
40
-
41
- describe('solutions-promote', () => {
42
- let changeDir, solDir;
43
-
44
- beforeEach(() => {
45
- changeDir = makeDir('change');
46
- solDir = makeDir('solutions');
47
- });
48
-
49
- after(() => {
50
- cleanup(changeDir);
51
- cleanup(solDir);
52
- });
53
-
54
- it('returns zero when no learnings.md exists', () => {
55
- const result = promoteRun({ changeDir, dir: solDir });
56
- assert.equal(result.promoted, 0);
57
- assert.equal(result.updated, 0);
58
- });
59
-
60
- it('promotes entry with severity=high and type=pitfall', () => {
61
- writeLearnings(changeDir, `## Auth token refresh failure
62
- ---
63
- phase: build
64
- domain: auth
65
- type: pitfall
66
- severity: high
67
- date: 2026-07-15
68
- ---
69
-
70
- When OAuth token refresh fails, the system silently swallows the error.
71
- `);
72
- const result = promoteRun({ changeDir, dir: solDir });
73
- assert.equal(result.promoted, 1);
74
- assert.ok(existsSync(join(solDir, 'build')), 'build phase dir should exist');
75
- });
76
-
77
- it('promotes entry with severity=medium and type=pattern', () => {
78
- writeLearnings(changeDir, `## Retry with backoff pattern
79
- ---
80
- phase: build
81
- domain: networking
82
- type: pattern
83
- severity: medium
84
- date: 2026-07-15
85
- ---
86
-
87
- Use exponential backoff for API retry logic.
88
- `);
89
- const result = promoteRun({ changeDir, dir: solDir });
90
- assert.equal(result.promoted, 1);
91
- });
92
-
93
- it('skips entry with severity=low', () => {
94
- writeLearnings(changeDir, `## Minor formatting issue
95
- ---
96
- phase: build
97
- domain: style
98
- type: pitfall
99
- severity: low
100
- date: 2026-07-15
101
- ---
102
-
103
- Some low severity issue.
104
- `);
105
- const result = promoteRun({ changeDir, dir: solDir });
106
- assert.equal(result.promoted, 0);
107
- });
108
-
109
- it('skips entry with type=insight (not pitfall/pattern)', () => {
110
- writeLearnings(changeDir, `## Interesting observation
111
- ---
112
- phase: build
113
- domain: auth
114
- type: insight
115
- severity: high
116
- date: 2026-07-15
117
- ---
118
-
119
- An insight that should not be promoted.
120
- `);
121
- const result = promoteRun({ changeDir, dir: solDir });
122
- assert.equal(result.promoted, 0);
123
- });
124
-
125
- it('detects duplicate domain+type and marks as confirmed', () => {
126
- writeGlobalIndex(solDir, [
127
- '| 2026-07-01 | build | auth | pitfall | medium | Existing auth pitfall | build/existing.md |',
128
- ]);
129
- writeLearnings(changeDir, `## Another auth pitfall
130
- ---
131
- phase: build
132
- domain: auth
133
- type: pitfall
134
- severity: high
135
- date: 2026-07-15
136
- ---
137
-
138
- Duplicate domain+type with existing index entry.
139
- `);
140
- const result = promoteRun({ changeDir, dir: solDir });
141
- assert.equal(result.updated, 1, 'should detect duplicate and mark as confirmed');
142
- assert.equal(result.promoted, 0, 'should not create new file for duplicate');
143
- });
144
-
145
- it('promotes multiple entries from single learnings.md', () => {
146
- writeLearnings(changeDir, `## Issue one
147
- ---
148
- phase: spec
149
- domain: api
150
- type: pitfall
151
- severity: high
152
- date: 2026-07-15
153
- ---
154
-
155
- First pitfall.
156
-
157
- ## Issue two
158
- ---
159
- phase: spec
160
- domain: db
161
- type: pattern
162
- severity: medium
163
- date: 2026-07-15
164
- ---
165
-
166
- Second pattern.
167
-
168
- ## Issue three (low sev, skipped)
169
- ---
170
- phase: spec
171
- domain: style
172
- type: pitfall
173
- severity: low
174
- date: 2026-07-15
175
- ---
176
-
177
- Skipped due to low severity.
178
- `);
179
- const result = promoteRun({ changeDir, dir: solDir });
180
- assert.equal(result.promoted, 2, 'should promote 2 entries (high+medium, skip low)');
181
- });
182
-
183
- it('appends promoted entry to global INDEX.md', () => {
184
- writeGlobalIndex(solDir);
185
- writeLearnings(changeDir, `## DB migration ordering
186
- ---
187
- phase: build
188
- domain: db
189
- type: pitfall
190
- severity: high
191
- date: 2026-07-15
192
- ---
193
-
194
- Migration files must be ordered by timestamp.
195
- `);
196
- promoteRun({ changeDir, dir: solDir });
197
- const indexContent = readFileSync(join(solDir, 'INDEX.md'), 'utf-8');
198
- assert.match(indexContent, /DB migration ordering/);
199
- });
200
- });
@@ -1,80 +0,0 @@
1
- import { describe, it, before, after } from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
4
- import { join } from 'node:path';
5
- import { tmpdir } from 'node:os';
6
-
7
- let mod;
8
- let tempDir;
9
-
10
- describe('spec-paths', () => {
11
- before(async () => {
12
- tempDir = mkdtempSync(join(tmpdir(), 'tf-spec-paths-'));
13
- mod = await import(join(process.cwd(), 'scripts/lib/spec-paths.mjs'));
14
- });
15
-
16
- after(() => {
17
- if (tempDir) rmSync(tempDir, { recursive: true, force: true });
18
- });
19
-
20
- it('finds canonical specs in sorted order', () => {
21
- const dir = mkdtempSync(join(tempDir, 'canonical-'));
22
- mkdirSync(join(dir, 'specs', 'z-auth'), { recursive: true });
23
- mkdirSync(join(dir, 'specs', 'a-ui'), { recursive: true });
24
- writeFileSync(join(dir, 'specs', 'z-auth', 'spec.md'), 'z');
25
- writeFileSync(join(dir, 'specs', 'a-ui', 'spec.md'), 'a');
26
-
27
- const files = mod.findCanonicalSpecFiles(dir).map(f => f.replace(dir + '/', ''));
28
- assert.deepEqual(files, ['specs/a-ui/spec.md', 'specs/z-auth/spec.md']);
29
- });
30
-
31
- it('ignores nested spec.md files when finding canonical specs', () => {
32
- const dir = mkdtempSync(join(tempDir, 'nested-'));
33
- mkdirSync(join(dir, 'specs', 'ui-theme', 'nested'), { recursive: true });
34
- writeFileSync(join(dir, 'specs', 'ui-theme', 'nested', 'spec.md'), 'nested');
35
-
36
- assert.deepEqual(mod.findCanonicalSpecFiles(dir), []);
37
-
38
- const result = mod.validateSpecPathLayout(dir, { requireSpecs: true });
39
- assert.equal(result.pass, false);
40
- assert.deepEqual(result.specFiles, []);
41
- assert.ok(result.failures.some(f => f.includes('No canonical spec files found')));
42
- });
43
-
44
- it('reports flat capability specs with expected canonical path', () => {
45
- const dir = mkdtempSync(join(tempDir, 'flat-'));
46
- mkdirSync(join(dir, 'specs'), { recursive: true });
47
- writeFileSync(join(dir, 'specs', 'ui-theme.md'), 'flat');
48
-
49
- const invalid = mod.findInvalidSpecFiles(dir);
50
- assert.deepEqual(invalid.map(i => i.path), ['specs/ui-theme.md']);
51
- assert.deepEqual(invalid.map(i => i.expected), ['specs/ui-theme/spec.md']);
52
- });
53
-
54
- it('reports root specs/spec.md as invalid', () => {
55
- const dir = mkdtempSync(join(tempDir, 'root-spec-'));
56
- mkdirSync(join(dir, 'specs'), { recursive: true });
57
- writeFileSync(join(dir, 'specs', 'spec.md'), 'root');
58
-
59
- const invalid = mod.findInvalidSpecFiles(dir);
60
- assert.deepEqual(invalid, [{ path: 'specs/spec.md', expected: 'specs/<capability>/spec.md' }]);
61
- });
62
-
63
- it('does not reject README.md inside specs', () => {
64
- const dir = mkdtempSync(join(tempDir, 'readme-'));
65
- mkdirSync(join(dir, 'specs'), { recursive: true });
66
- writeFileSync(join(dir, 'specs', 'README.md'), 'notes');
67
-
68
- assert.deepEqual(mod.findInvalidSpecFiles(dir), []);
69
- });
70
-
71
- it('fails layout validation when canonical specs are required but missing', () => {
72
- const dir = mkdtempSync(join(tempDir, 'missing-'));
73
- mkdirSync(join(dir, 'specs'), { recursive: true });
74
-
75
- const result = mod.validateSpecPathLayout(dir, { requireSpecs: true });
76
- assert.equal(result.pass, false);
77
- assert.deepEqual(result.specFiles, []);
78
- assert.ok(result.failures.some(f => f.includes('No canonical spec files found')));
79
- });
80
- });
@@ -1,251 +0,0 @@
1
- // tests/lib/state-loader.test.mjs
2
- // Tests for scripts/lib/state-loader.mjs
3
- import { describe, it, before, after } from 'node:test';
4
- import assert from 'node:assert/strict';
5
- import { mkdtempSync, writeFileSync, readFileSync, rmSync, existsSync } from 'node:fs';
6
- import { join } from 'node:path';
7
- import { tmpdir } from 'node:os';
8
-
9
- let tempDir;
10
-
11
- describe('state-loader: readState()', () => {
12
- let stateLoader;
13
-
14
- before(async () => {
15
- tempDir = mkdtempSync(join(tmpdir(), 'tf-state-test-'));
16
- const modulePath = join(process.cwd(), 'scripts/lib/state-loader.mjs');
17
- stateLoader = await import(modulePath);
18
- });
19
-
20
- after(() => {
21
- if (tempDir) rmSync(tempDir, { recursive: true, force: true });
22
- });
23
-
24
- it('returns defaults when no state file exists', () => {
25
- const state = stateLoader.readState(tempDir);
26
- assert.equal(state.state, 'exploring');
27
- assert.equal(state.workflow, 'auto');
28
- assert.equal(state.batches_completed, 0);
29
- assert.equal(state.execution_mode, null);
30
- assert.equal(state.execution_plan_hash, null);
31
- assert.equal(state.execution_plan_revision, null);
32
- });
33
-
34
- it('infers change_name from directory when no state file', () => {
35
- const changeDir = join(tempDir, 'my-feature');
36
- const state = stateLoader.readState(changeDir);
37
- assert.equal(state.change_name, 'my-feature');
38
- });
39
-
40
- it('parses existing state file correctly', () => {
41
- const stateYaml = [
42
- 'state: executing',
43
- 'workflow: full',
44
- 'artifacts_hash: sha256:abc123',
45
- 'batches_completed: 3',
46
- 'test_result: pass',
47
- 'change_name: export-csv',
48
- 'dp_0_confirmed: true',
49
- 'dp_1_result: confirmed: add csv export',
50
- ].join('\n');
51
-
52
- writeFileSync(join(tempDir, '.team-flow.yaml'), stateYaml);
53
-
54
- const state = stateLoader.readState(tempDir);
55
-
56
- assert.equal(state.state, 'executing');
57
- assert.equal(state.workflow, 'full');
58
- assert.equal(state.artifacts_hash, 'sha256:abc123');
59
- assert.equal(state.batches_completed, 3);
60
- assert.equal(state.test_result, 'pass');
61
- assert.equal(state.change_name, 'export-csv');
62
- assert.equal(state.dp_0_confirmed, 'true');
63
- assert.equal(state.dp_1_result, 'confirmed: add csv export');
64
- });
65
-
66
- it('handles null values in YAML', () => {
67
- const stateYaml = [
68
- 'state: exploring',
69
- 'artifacts_hash: null',
70
- 'contract_hash: null',
71
- 'dp_0_timestamp: null',
72
- ].join('\n');
73
-
74
- writeFileSync(join(tempDir, '.team-flow.yaml'), stateYaml);
75
-
76
- const state = stateLoader.readState(tempDir);
77
-
78
- assert.equal(state.artifacts_hash, null);
79
- assert.equal(state.contract_hash, null);
80
- assert.equal(state.dp_0_timestamp, null);
81
- });
82
-
83
- it('ignores comments and empty lines in YAML', () => {
84
- const stateYaml = [
85
- '# This is a comment',
86
- '',
87
- 'state: specifying',
88
- ' ',
89
- '# Another comment',
90
- 'workflow: tweak',
91
- ].join('\n');
92
-
93
- writeFileSync(join(tempDir, '.team-flow.yaml'), stateYaml);
94
-
95
- const state = stateLoader.readState(tempDir);
96
-
97
- assert.equal(state.state, 'specifying');
98
- assert.equal(state.workflow, 'tweak');
99
- });
100
-
101
- it('merges partial state file with defaults', () => {
102
- const stateYaml = 'state: approved-for-build\nworkflow: hotfix';
103
-
104
- writeFileSync(join(tempDir, '.team-flow.yaml'), stateYaml);
105
-
106
- const state = stateLoader.readState(tempDir);
107
- assert.equal(state.state, 'approved-for-build');
108
- assert.equal(state.workflow, 'hotfix');
109
- // Unspecified fields should use defaults
110
- assert.equal(state.batches_completed, 0);
111
- assert.equal(state.test_result, null);
112
- });
113
- });
114
-
115
- describe('state-loader: writeState()', () => {
116
- let stateLoader;
117
-
118
- before(async () => {
119
- tempDir = mkdtempSync(join(tmpdir(), 'tf-state-write-'));
120
- const modulePath = join(process.cwd(), 'scripts/lib/state-loader.mjs');
121
- stateLoader = await import(modulePath);
122
- });
123
-
124
- after(() => {
125
- if (tempDir) rmSync(tempDir, { recursive: true, force: true });
126
- });
127
-
128
- it('writes state file with correct structure', () => {
129
- const state = {
130
- state: 'executing',
131
- workflow: 'full',
132
- artifacts_hash: 'sha256:def456',
133
- contract_hash: null,
134
- batches_completed: 2,
135
- test_result: null,
136
- change_name: 'test-change',
137
- dp_0_confirmed: 'true',
138
- };
139
-
140
- stateLoader.writeState(tempDir, state);
141
-
142
- const filePath = join(tempDir, '.team-flow.yaml');
143
- assert.ok(existsSync(filePath));
144
-
145
- const content = readFileSync(filePath, 'utf-8');
146
- assert.ok(content.includes('state: executing'));
147
- assert.ok(content.includes('workflow: full'));
148
- assert.ok(content.includes('artifacts_hash: sha256:def456'));
149
- assert.ok(content.includes('contract_hash: null'));
150
- assert.ok(content.includes('batches_completed: 2'));
151
- assert.ok(content.includes('execution_plan_hash: null'));
152
- assert.ok(content.includes('execution_plan_revision: null'));
153
- assert.ok(content.includes('change_name: test-change'));
154
- assert.ok(content.includes('dp_0_confirmed: true'));
155
- });
156
-
157
- it('writes all DP fields (0-7)', () => {
158
- const state = {
159
- state: 'closing',
160
- dp_0_decisions: 'scope: csv export only, tech: nestjs',
161
- dp_0_result: 'confirmed: scope ok',
162
- dp_0_confirmed: 'true',
163
- dp_0_timestamp: '2026-07-01T08:00:00Z',
164
- dp_1_result: 'confirmed: csv export',
165
- dp_2_result: 'approved: artifacts ok',
166
- dp_3_result: 'contract signed',
167
- dp_4_result: 'execution started',
168
- dp_5_result: null,
169
- dp_6_result: null,
170
- dp_7_result: null,
171
- };
172
-
173
- stateLoader.writeState(tempDir, state);
174
-
175
- const content = readFileSync(join(tempDir, '.team-flow.yaml'), 'utf-8');
176
- assert.ok(content.includes('dp_0_decisions: scope: csv export only, tech: nestjs'));
177
- assert.ok(content.includes('dp_0_result: confirmed: scope ok'));
178
- assert.ok(content.includes('dp_0_confirmed: true'));
179
- assert.ok(content.includes('dp_1_result: confirmed: csv export'));
180
- assert.ok(content.includes('dp_5_result: null'));
181
- });
182
-
183
- it('round-trips dp_0_result when updated through state loader', () => {
184
- stateLoader.writeState(tempDir, {
185
- state: 'exploring',
186
- dp_0_decisions: 'scope confirmed',
187
- dp_0_confirmed: 'true',
188
- });
189
-
190
- stateLoader.updateField(tempDir, 'dp_0_result', 'confirmed: manual');
191
-
192
- const content = readFileSync(join(tempDir, '.team-flow.yaml'), 'utf-8');
193
- const state = stateLoader.readState(tempDir);
194
-
195
- assert.ok(content.includes('dp_0_result: confirmed: manual'));
196
- assert.equal(state.dp_0_result, 'confirmed: manual');
197
- });
198
-
199
- it('round-trips state through write then read', () => {
200
- const original = {
201
- state: 'bridging',
202
- workflow: 'hotfix',
203
- artifacts_hash: 'sha256:xyz789',
204
- batches_completed: 0,
205
- dp_1_result: 'confirmed: refactor auth',
206
- dp_1_timestamp: '2026-07-01T10:00:00Z',
207
- };
208
-
209
- stateLoader.writeState(tempDir, original);
210
- const read = stateLoader.readState(tempDir);
211
-
212
- assert.equal(read.state, original.state);
213
- assert.equal(read.workflow, original.workflow);
214
- assert.equal(read.artifacts_hash, original.artifacts_hash);
215
- assert.equal(read.batches_completed, original.batches_completed);
216
- assert.equal(read.dp_1_result, original.dp_1_result);
217
- assert.equal(read.dp_1_timestamp, original.dp_1_timestamp);
218
- });
219
- });
220
-
221
- describe('state-loader: updateField()', () => {
222
- let stateLoader;
223
-
224
- before(async () => {
225
- tempDir = mkdtempSync(join(tmpdir(), 'tf-state-update-'));
226
- const modulePath = join(process.cwd(), 'scripts/lib/state-loader.mjs');
227
- stateLoader = await import(modulePath);
228
- });
229
-
230
- after(() => {
231
- if (tempDir) rmSync(tempDir, { recursive: true, force: true });
232
- });
233
-
234
- it('updates a single field without affecting others', () => {
235
- // Initialize with some state
236
- stateLoader.writeState(tempDir, {
237
- state: 'exploring',
238
- workflow: 'full',
239
- batches_completed: 0,
240
- });
241
-
242
- stateLoader.updateField(tempDir, 'state', 'specifying');
243
- stateLoader.updateField(tempDir, 'batches_completed', 3);
244
-
245
- const state = stateLoader.readState(tempDir);
246
- assert.equal(state.state, 'specifying');
247
- assert.equal(state.batches_completed, 3);
248
- // workflow should still be 'full'
249
- assert.equal(state.workflow, 'full');
250
- });
251
- });