@xulthekl/team-flow 0.23.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/always/phase-guard.md +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/marketplace.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/.github/plugin/marketplace.json +2 -2
- package/CHANGELOG.md +50 -0
- package/GEMINI.md +1 -1
- package/INSTALL.md +1 -1
- package/README.md +1 -1
- package/docs/README_en.md +1 -1
- package/docs/solutions/INDEX.md +5 -0
- package/docs/solutions/cross-phase/2026-07-28-no-summary.md +17 -0
- package/gemini-extension.json +1 -1
- package/hooks/pre-tool-use-guard +9 -9
- package/hooks/session-start +2 -2
- package/llms.txt +1 -1
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/scripts/guard/checks/compound-captured.mjs +70 -0
- package/scripts/guard/guard.mjs +3 -1
- package/scripts/lib/cmd-state.mjs +2 -0
- package/scripts/lib/solutions-promote.mjs +1 -1
- package/scripts/lib/state-loader.mjs +5 -0
- package/skills/bug-investigator/SKILL.md +1 -1
- package/skills/build-executor/SKILL.md +19 -19
- package/skills/build-executor/implementer-prompt.md +1 -1
- package/skills/build-executor/references/execution-modes.md +6 -6
- package/skills/build-executor/task-reviewer-prompt.md +1 -1
- package/skills/code-reviewer/SKILL.md +2 -2
- package/skills/code-reviewer/code-reviewer-prompt.md +1 -1
- package/skills/contract-builder/SKILL.md +6 -6
- package/skills/need-explorer/SKILL.md +2 -2
- package/skills/release-archivist/SKILL.md +12 -12
- package/skills/release-archivist/references/closing-procedures.md +8 -8
- package/skills/spec-merger/SKILL.md +2 -2
- package/skills/spec-writer/SKILL.md +3 -3
- package/skills/workflow-bootstrap/SKILL.md +13 -2
- package/skills/workflow-bootstrap/references/b1-reconnaissance.md +13 -7
- package/skills/workflow-bootstrap/scripts/recon-probe.sh +122 -1
- package/skills/workflow-orchestrator/references/s2-prd-prototype-loop.md +1 -1
- package/skills/workflow-orchestrator/references/s4-split-validate.md +1 -1
- package/skills/workflow-orchestrator/references/s5-monitoring.md +1 -1
- package/skills/workflow-start/SKILL.md +16 -16
- package/skills/workflow-start/references/routing-rules.md +17 -17
- package/tests/lib/guard-compound-captured.test.mjs +92 -0
- package/tests/lib/guard-specs-merged.test.mjs +2 -0
- package/tests/lib/guard-tests-passing.test.mjs +2 -0
- package/tests/lib/guard.test.mjs +2 -0
- package/tests/lib/solutions-capture.test.mjs +108 -0
- package/tests/lib/solutions-index-gen.test.mjs +147 -0
- package/tests/lib/solutions-inject.test.mjs +115 -0
- package/tests/lib/solutions-promote.test.mjs +200 -0
|
@@ -0,0 +1,115 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,200 @@
|
|
|
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
|
+
});
|