@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
|
@@ -15,14 +15,14 @@ Do NOT invoke for: general coding tasks outside team-flow changes, casual questi
|
|
|
15
15
|
|
|
16
16
|
## States
|
|
17
17
|
|
|
18
|
-
`exploring` → `specifying` → `bridging` → `approved-for-build` → `executing` → `closing`, with `debugging` side-path from `executing`, and `abandoned` as terminal. If a transition is ambiguous, run `npx --yes --package @xulthekl/team-flow@0.
|
|
18
|
+
`exploring` → `specifying` → `bridging` → `approved-for-build` → `executing` → `closing`, with `debugging` side-path from `executing`, and `abandoned` as terminal. If a transition is ambiguous, run `npx --yes --package @xulthekl/team-flow@0.24.0 tf runtime asset read docs/state-machine.md`.
|
|
19
19
|
|
|
20
20
|
## Initialization
|
|
21
21
|
|
|
22
|
-
1. **Update check**: Run `npx --yes --package @xulthekl/team-flow@0.
|
|
22
|
+
1. **Update check**: Run `npx --yes --package @xulthekl/team-flow@0.24.0 tf runtime check-update`. Exit 0 → continue. Exit 1 → non-blocking upgrade reminder. Exit 2 → skip.
|
|
23
23
|
2. **Inspect change folder**: Check for `proposal.md`, `specs/`, `design.md`, `tasks.md`, `execution-contract.md`. Answer: Is the change fuzzy? Artifacts missing/unstable? Contract exist? User approved contract? Execution in progress or blocked? In verification/wrap-up?
|
|
24
|
-
3. **Overlay recovery scan**: Run `npx --yes --package @xulthekl/team-flow@0.
|
|
25
|
-
4. **Execution-control recovery scan**: For `approved-for-build`, `executing`, `debugging`, or `closing`, run `npx --yes --package @xulthekl/team-flow@0.
|
|
24
|
+
3. **Overlay recovery scan**: Run `npx --yes --package @xulthekl/team-flow@0.24.0 tf handoff list <change-dir> --json` and `npx --yes --package @xulthekl/team-flow@0.24.0 tf checkpoint list <change-dir> --json`. A `result-ready` handoff requires explicit review and `npx --yes --package @xulthekl/team-flow@0.24.0 tf handoff resolve` before resuming the affected work. An `active` handoff is non-blocking side work. Show a non-stale checkpoint as recovery context; show a stale checkpoint only as historical evidence.
|
|
25
|
+
4. **Execution-control recovery scan**: For `approved-for-build`, `executing`, `debugging`, or `closing`, run `npx --yes --package @xulthekl/team-flow@0.24.0 tf execution show <change-dir> --json`. Treat only `current: true` plus `waves[].eligible: true` as permission to start a wave; report plan revision, mode, next eligible wave, and every wave's receipt/blockers. A missing, invalid, or stale plan blocks implementation and routes to `build-executor`; do not infer progress from chat history.
|
|
26
26
|
|
|
27
27
|
## DP-0: User Confirmation Gate
|
|
28
28
|
|
|
@@ -36,17 +36,17 @@ Ask (manual path): change name + one-sentence intent, known constraints, related
|
|
|
36
36
|
|
|
37
37
|
After confirmation:
|
|
38
38
|
```bash
|
|
39
|
-
npx --yes --package @xulthekl/team-flow@0.
|
|
40
|
-
npx --yes --package @xulthekl/team-flow@0.
|
|
41
|
-
npx --yes --package @xulthekl/team-flow@0.
|
|
42
|
-
npx --yes --package @xulthekl/team-flow@0.
|
|
39
|
+
npx --yes --package @xulthekl/team-flow@0.24.0 tf state set <change-dir> dp_0_decisions "<summary>"
|
|
40
|
+
npx --yes --package @xulthekl/team-flow@0.24.0 tf state set <change-dir> dp_0_result confirmed
|
|
41
|
+
npx --yes --package @xulthekl/team-flow@0.24.0 tf state set <change-dir> dp_0_confirmed true
|
|
42
|
+
npx --yes --package @xulthekl/team-flow@0.24.0 tf state set <change-dir> dp_0_timestamp $(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
43
43
|
```
|
|
44
44
|
|
|
45
45
|
Config-aware routing: check `artifacts.order` and `artifacts.skip` from project config.
|
|
46
46
|
|
|
47
47
|
## Mode Detection
|
|
48
48
|
|
|
49
|
-
If workflow is `auto`/`null`/unset: run `npx --yes --package @xulthekl/team-flow@0.
|
|
49
|
+
If workflow is `auto`/`null`/unset: run `npx --yes --package @xulthekl/team-flow@0.24.0 tf runtime infer <change-dir>`. Inference: **hotfix** (≤2 tasks, ≤2 files, no schema/API/new modules), **tweak** (≤4 tasks, config/doc only), **full** (anything larger). Persist with `npx --yes --package @xulthekl/team-flow@0.24.0 tf state set <dir> workflow <mode>`.
|
|
50
50
|
|
|
51
51
|
Validate mode against artifact content. If hotfix/tweak criteria not met → upgrade to `full` and output reason. Don't overwrite explicit mode unless user asks.
|
|
52
52
|
|
|
@@ -59,19 +59,19 @@ Change is fuzzy, scope unclear, comparing options, no stable change name.
|
|
|
59
59
|
Guard: `arch_design_decision` in `.team-flow.yaml` is `null` → must run before spec-writer. Dispatch `architecture-design` as sub-agent; after return, run reasonableness check and write yaml. Full protocol in `references/routing-rules.md`「Route to architecture-design」.
|
|
60
60
|
|
|
61
61
|
### Route to spec-writer
|
|
62
|
-
Guard: `npx --yes --package @xulthekl/team-flow@0.
|
|
62
|
+
Guard: `npx --yes --package @xulthekl/team-flow@0.24.0 tf runtime guard check <dir> exploring specifying --json` → fail = BLOCK. **arch_design_decision must not be null** → fail = BLOCK (v0.9 §26). User knows what they want, artifacts missing/incomplete.
|
|
63
63
|
|
|
64
64
|
### Route to contract-builder
|
|
65
65
|
Guard: `... check <dir> specifying bridging --json` → fail = BLOCK. Artifacts exist, implementation requested, contract missing/stale. Include `DP-3: 契约批准`.
|
|
66
66
|
|
|
67
67
|
### Route to build-executor
|
|
68
|
-
Contract exists and approved, contract matches artifacts. Include `DP-4: 执行模式选择`: propose waves, run `npx --yes --package @xulthekl/team-flow@0.
|
|
68
|
+
Contract exists and approved, contract matches artifacts. Include `DP-4: 执行模式选择`: propose waves, run `npx --yes --package @xulthekl/team-flow@0.24.0 tf execution recommend <change-dir> [--wave ...]`, show the user every available mode plus evidence and the recommendation, then obtain a clear selection. The command saves a current receipt; before the first implementation edit, `build-executor` must run `npx --yes --package @xulthekl/team-flow@0.24.0 tf execution plan <change-dir> --mode <selected> --confirm ...` (and `--acknowledge-recommendation` when the selected mode differs from the recommendation) using matching artifacts, contract, and waves, then `npx --yes --package @xulthekl/team-flow@0.24.0 tf execution show <change-dir> --json`; report the saved revision, selected mode, recommendation alignment, ordered waves, and actual concurrent-dispatch capability. A revision must repeat recommend and confirmation. Do not transition to `executing` until `show` reports `current: true`; then run `... check <dir> approved-for-build executing --json` → fail = BLOCK.
|
|
69
69
|
|
|
70
70
|
### Route to bug-investigator
|
|
71
71
|
Execution hit blockage: test failure, unexpected behavior, build error, task cannot proceed. After debugging, route back to build-executor.
|
|
72
72
|
|
|
73
73
|
### Route to code-reviewer
|
|
74
|
-
The current planned wave is implemented and ready for spec-compliance + code-quality verification. A reviewer must write an `npx --yes --package @xulthekl/team-flow@0.
|
|
74
|
+
The current planned wave is implemented and ready for spec-compliance + code-quality verification. A reviewer must write an `npx --yes --package @xulthekl/team-flow@0.24.0 tf execution review <change-dir> --wave <id> --base <sha> --head <sha> --report <path> --verdict <pass|fail>` receipt before any dependent wave or closing transition.
|
|
75
75
|
|
|
76
76
|
### Route to release-archivist
|
|
77
77
|
Guard: `... check <dir> executing closing --json` → fail = BLOCK. Implementation complete, verification complete/nearly complete. Include `DP-7: 归档确认`.
|
|
@@ -90,10 +90,10 @@ uncertainty. Do not create a prototype handoff or enter a prototype worktree
|
|
|
90
90
|
until the user confirms. After confirmation:
|
|
91
91
|
|
|
92
92
|
```bash
|
|
93
|
-
npx --yes --package @xulthekl/team-flow@0.
|
|
93
|
+
npx --yes --package @xulthekl/team-flow@0.24.0 tf handoff create <change-dir> \
|
|
94
94
|
--type prototype --objective "<confirmed objective>" \
|
|
95
95
|
--expected-output "<expected evidence>" --acceptance "<completion criterion>"
|
|
96
|
-
npx --yes --package @xulthekl/team-flow@0.
|
|
96
|
+
npx --yes --package @xulthekl/team-flow@0.24.0 tf isolate <change-dir> prototype-<handoff-id>
|
|
97
97
|
```
|
|
98
98
|
|
|
99
99
|
Never suggest or enter this route automatically for backend, CLI, configuration,
|
|
@@ -107,7 +107,7 @@ work.
|
|
|
107
107
|
- **Hotfix**: Route to contract-builder (minimal), skip need-explorer + spec-writer, guard check `exploring bridging --workflow hotfix`, then `bridging -> approved-for-build`, after DP-3 → build-executor (recommend, show, and confirm an execution mode), after → release-archivist (lightweight). Hotfix may skip `proposal.md`, `design.md`, `tasks.md`, and `specs/`, but it still requires a fresh minimal `execution-contract.md`, DP-3 approval, and a current execution plan before build. **architecture-design 不豁免**(v0.9 §26):同样过 architecture-design 子代理判断门,快速判定是否涉及架构变更(hotfix 可能正是架构缺陷导致)
|
|
108
108
|
- **Tweak**: Route to build-executor (direct edit), skip need-explorer + spec-writer + contract-builder, guard check `exploring approved-for-build --workflow tweak`, after → release-archivist (lightweight). **architecture-design 不豁免**(v0.9 §26):同样过 architecture-design 子代理判断门
|
|
109
109
|
|
|
110
|
-
Post-transition: 💡 `npx --yes --package @xulthekl/team-flow@0.
|
|
110
|
+
Post-transition: 💡 `npx --yes --package @xulthekl/team-flow@0.24.0 tf inject <change-dir>` to update phase-guard artifacts.
|
|
111
111
|
|
|
112
112
|
## Staleness Detection
|
|
113
113
|
|
|
@@ -124,7 +124,7 @@ Use content inspection, not timestamps.
|
|
|
124
124
|
## Guardrails
|
|
125
125
|
|
|
126
126
|
- No implementation before planning artifacts or contract exist
|
|
127
|
-
- No implementation for full/hotfix without a current `npx --yes --package @xulthekl/team-flow@0.
|
|
127
|
+
- No implementation for full/hotfix without a current `npx --yes --package @xulthekl/team-flow@0.24.0 tf execution plan`; no state transition based on an unverified DP-4 string
|
|
128
128
|
- No "continue" without state inspection
|
|
129
129
|
- No implementation past stale contract
|
|
130
130
|
- No implementation past bug without investigation
|
|
@@ -32,17 +32,17 @@ artifacts: # required 时必填
|
|
|
32
32
|
- `null`(未判定)→ **BLOCK**
|
|
33
33
|
|
|
34
34
|
```bash
|
|
35
|
-
npx --yes --package @xulthekl/team-flow@0.
|
|
36
|
-
npx --yes --package @xulthekl/team-flow@0.
|
|
37
|
-
npx --yes --package @xulthekl/team-flow@0.
|
|
35
|
+
npx --yes --package @xulthekl/team-flow@0.24.0 tf state set <change-dir> arch_design_decision "<required|skipped>"
|
|
36
|
+
npx --yes --package @xulthekl/team-flow@0.24.0 tf state set <change-dir> arch_design_reason "<reason>"
|
|
37
|
+
npx --yes --package @xulthekl/team-flow@0.24.0 tf state set <change-dir> arch_design_timestamp $(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
38
38
|
# if required:
|
|
39
|
-
npx --yes --package @xulthekl/team-flow@0.
|
|
39
|
+
npx --yes --package @xulthekl/team-flow@0.24.0 tf state set <change-dir> arch_design_artifacts "architecture/architecture.md,architecture/database.md,architecture/api.md"
|
|
40
40
|
```
|
|
41
41
|
|
|
42
42
|
**hotfix / tweak 不豁免**:同样过 architecture-design 子代理判断门。
|
|
43
43
|
|
|
44
44
|
## Route to spec-writer
|
|
45
|
-
Guard: `npx --yes --package @xulthekl/team-flow@0.
|
|
45
|
+
Guard: `npx --yes --package @xulthekl/team-flow@0.24.0 tf runtime guard check <dir> exploring specifying --json` → fail = BLOCK.
|
|
46
46
|
**arch_design_decision must not be null** → fail = BLOCK(architecture-design gate not passed,v0.9 §26)。
|
|
47
47
|
User knows what they want, artifacts missing/incomplete.
|
|
48
48
|
|
|
@@ -55,12 +55,12 @@ Contract exists and approved, contract matches artifacts. Include DP-4: 执行
|
|
|
55
55
|
|
|
56
56
|
Propose waves, run:
|
|
57
57
|
```bash
|
|
58
|
-
npx --yes --package @xulthekl/team-flow@0.
|
|
58
|
+
npx --yes --package @xulthekl/team-flow@0.24.0 tf execution recommend <change-dir> [--wave ...]
|
|
59
59
|
```
|
|
60
60
|
Show every available mode plus evidence and recommendation, obtain clear selection. Then:
|
|
61
61
|
```bash
|
|
62
|
-
npx --yes --package @xulthekl/team-flow@0.
|
|
63
|
-
npx --yes --package @xulthekl/team-flow@0.
|
|
62
|
+
npx --yes --package @xulthekl/team-flow@0.24.0 tf execution plan <change-dir> --mode <selected> --confirm ...
|
|
63
|
+
npx --yes --package @xulthekl/team-flow@0.24.0 tf execution show <change-dir> --json
|
|
64
64
|
```
|
|
65
65
|
Do not transition to `executing` until `show` reports `current: true`.
|
|
66
66
|
Guard: `... check <dir> approved-for-build executing --json` → fail = BLOCK.
|
|
@@ -71,7 +71,7 @@ Execution hit blockage: test failure, unexpected behavior, build error. After de
|
|
|
71
71
|
## Route to code-reviewer
|
|
72
72
|
Current planned wave implemented and ready for review. Reviewer must write:
|
|
73
73
|
```bash
|
|
74
|
-
npx --yes --package @xulthekl/team-flow@0.
|
|
74
|
+
npx --yes --package @xulthekl/team-flow@0.24.0 tf execution review <change-dir> --wave <id> --base <sha> --head <sha> --report <path> --verdict <pass|fail>
|
|
75
75
|
```
|
|
76
76
|
|
|
77
77
|
## Route to release-archivist
|
|
@@ -93,10 +93,10 @@ User explicitly requests, bug-investigator escalates after 3+ failures AND user
|
|
|
93
93
|
|
|
94
94
|
When the user's brief explicitly contains UI/screen/interaction/layout/UX uncertainty, ask once whether a prototype would reduce uncertainty. After confirmation:
|
|
95
95
|
```bash
|
|
96
|
-
npx --yes --package @xulthekl/team-flow@0.
|
|
96
|
+
npx --yes --package @xulthekl/team-flow@0.24.0 tf handoff create <change-dir> \
|
|
97
97
|
--type prototype --objective "<confirmed objective>" \
|
|
98
98
|
--expected-output "<expected evidence>" --acceptance "<completion criterion>"
|
|
99
|
-
npx --yes --package @xulthekl/team-flow@0.
|
|
99
|
+
npx --yes --package @xulthekl/team-flow@0.24.0 tf isolate <change-dir> prototype-<handoff-id>
|
|
100
100
|
```
|
|
101
101
|
Never suggest for backend/CLI/config/internal-refactor work. Never pass `--force`.
|
|
102
102
|
|
|
@@ -130,17 +130,17 @@ else:
|
|
|
130
130
|
|
|
131
131
|
After confirmation:
|
|
132
132
|
```bash
|
|
133
|
-
npx --yes --package @xulthekl/team-flow@0.
|
|
134
|
-
npx --yes --package @xulthekl/team-flow@0.
|
|
135
|
-
npx --yes --package @xulthekl/team-flow@0.
|
|
136
|
-
npx --yes --package @xulthekl/team-flow@0.
|
|
133
|
+
npx --yes --package @xulthekl/team-flow@0.24.0 tf state set <change-dir> dp_0_decisions "<summary>"
|
|
134
|
+
npx --yes --package @xulthekl/team-flow@0.24.0 tf state set <change-dir> dp_0_result confirmed
|
|
135
|
+
npx --yes --package @xulthekl/team-flow@0.24.0 tf state set <change-dir> dp_0_confirmed true
|
|
136
|
+
npx --yes --package @xulthekl/team-flow@0.24.0 tf state set <change-dir> dp_0_timestamp $(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
137
137
|
```
|
|
138
138
|
|
|
139
139
|
## Mode Detection
|
|
140
140
|
|
|
141
141
|
If workflow is `auto`/`null`/unset:
|
|
142
142
|
```bash
|
|
143
|
-
npx --yes --package @xulthekl/team-flow@0.
|
|
143
|
+
npx --yes --package @xulthekl/team-flow@0.24.0 tf runtime infer <change-dir>
|
|
144
144
|
```
|
|
145
145
|
Inference: **hotfix** (≤2 tasks, ≤2 files, no schema/API/new modules), **tweak** (≤4 tasks, config/doc only), **full** (anything larger).
|
|
146
146
|
|
|
@@ -152,4 +152,4 @@ Inference: **hotfix** (≤2 tasks, ≤2 files, no schema/API/new modules), **twe
|
|
|
152
152
|
- **Brief drift (advisory, v0.9)**: change-brief.md 的 `plan_hash` 与当前 plan.md 不一致 → 提示回 orchestrator 重新分发;**不**阻断、**不**进 artifacts_hash、**不**混入上述三条产物互查
|
|
153
153
|
|
|
154
154
|
## Post-transition
|
|
155
|
-
💡 `npx --yes --package @xulthekl/team-flow@0.
|
|
155
|
+
💡 `npx --yes --package @xulthekl/team-flow@0.24.0 tf inject <change-dir>` to update phase-guard artifacts.
|
|
@@ -0,0 +1,92 @@
|
|
|
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
|
+
});
|
|
@@ -27,6 +27,8 @@ function makeChangeDir(withDelta) {
|
|
|
27
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
28
|
: '## Requirements\n\n### Requirement: Existing\n\nThe system SHALL exist.\n\n#### Scenario: Existing\n- **WHEN** a\n- **THEN** b\n';
|
|
29
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');
|
|
30
32
|
initializeGitRepository(dir);
|
|
31
33
|
return dir;
|
|
32
34
|
}
|
|
@@ -25,6 +25,8 @@ function makeChangeDir() {
|
|
|
25
25
|
writeFileSync(join(dir, 'tasks.md'), '# Tasks\n\n- [x] Task 1\n- [x] Task 2\n');
|
|
26
26
|
writeFileSync(join(dir, 'specs', 'test.md'), '## ADDED Requirements\n\n### Requirement: Test\n\nThe system SHALL test.\n\n#### Scenario: Test\n- **WHEN** test\n- **THEN** test\n');
|
|
27
27
|
writeFileSync(join(dir, 'execution-contract.md'), '# Execution Contract\n\n## Intent Lock\nTest.\n');
|
|
28
|
+
// v0.24.0: compound-captured dimension requires learnings.md
|
|
29
|
+
writeFileSync(join(dir, 'learnings.md'), '# Session Learnings\n\n## Lesson 1\nTest.\n');
|
|
28
30
|
initializeGitRepository(dir);
|
|
29
31
|
return dir;
|
|
30
32
|
}
|
package/tests/lib/guard.test.mjs
CHANGED
|
@@ -312,6 +312,8 @@ describe('guard: execution control records', () => {
|
|
|
312
312
|
function recordPassingClosingPrerequisites() {
|
|
313
313
|
runNodeScript(CLI_PATH, ['state', 'set', dir, 'test_result', 'pass: unit tests']);
|
|
314
314
|
runNodeScript(CLI_PATH, ['state', 'set', dir, 'spec_merged', 'true']);
|
|
315
|
+
// v0.24.0: compound-captured dimension requires learnings.md (or explicit skip)
|
|
316
|
+
writeFileSync(join(dir, 'learnings.md'), '# Session Learnings\n\n## Lesson 1\nCaptured during guard test.\n');
|
|
315
317
|
}
|
|
316
318
|
|
|
317
319
|
function writeReviewReport(name, content = 'Review completed without blocking findings.\n') {
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// tests/lib/solutions-capture.test.mjs
|
|
2
|
+
// Tests for scripts/lib/solutions-capture.mjs — compound capture mechanism
|
|
3
|
+
|
|
4
|
+
import { describe, it, before, after, beforeEach } from 'node:test';
|
|
5
|
+
import assert from 'node:assert/strict';
|
|
6
|
+
import { mkdtempSync, readFileSync, existsSync, rmSync, writeFileSync, 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: captureRun } = await import(join(ROOT, 'scripts', 'lib', 'solutions-capture.mjs'));
|
|
16
|
+
|
|
17
|
+
function makeDir() {
|
|
18
|
+
return mkdtempSync(join(tmpdir(), 'tf-sol-capture-'));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function cleanup(dir) {
|
|
22
|
+
if (existsSync(dir)) rmSync(dir, { recursive: true, force: true });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('solutions-capture', () => {
|
|
26
|
+
let dir;
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
dir = makeDir();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
after(() => {
|
|
33
|
+
// Clean up all temp dirs
|
|
34
|
+
cleanup(dir);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('creates experience file in correct phase directory', () => {
|
|
38
|
+
const result = captureRun({
|
|
39
|
+
phase: 'prd', domain: 'auth', type: 'pitfall', severity: 'high',
|
|
40
|
+
summary: 'OAuth token refresh fails silently', dir,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
assert.equal(result.phase, 'prd');
|
|
44
|
+
assert.equal(result.severity, 'high');
|
|
45
|
+
assert.match(result.file, /^prd\/\d{4}-\d{2}-\d{2}-.*\.md$/);
|
|
46
|
+
|
|
47
|
+
// Verify file exists and has frontmatter
|
|
48
|
+
const filePath = join(dir, result.file);
|
|
49
|
+
assert.ok(existsSync(filePath), 'experience file should exist');
|
|
50
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
51
|
+
assert.match(content, /^---\nphase: prd\n/);
|
|
52
|
+
assert.match(content, /domain: auth/);
|
|
53
|
+
assert.match(content, /type: pitfall/);
|
|
54
|
+
assert.match(content, /severity: high/);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('creates INDEX.md from scratch when missing', () => {
|
|
58
|
+
captureRun({
|
|
59
|
+
phase: 'build', domain: 'ci', type: 'pattern', severity: 'medium',
|
|
60
|
+
summary: 'Parallel test execution race condition', dir,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const indexPath = join(dir, 'INDEX.md');
|
|
64
|
+
assert.ok(existsSync(indexPath), 'INDEX.md should be created');
|
|
65
|
+
const content = readFileSync(indexPath, 'utf-8');
|
|
66
|
+
assert.match(content, /# Solutions Index/);
|
|
67
|
+
assert.match(content, /parallel test execution/i);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('appends to existing INDEX.md', () => {
|
|
71
|
+
captureRun({ phase: 'build', domain: 'ci', type: 'pattern', severity: 'high', summary: 'First issue', dir });
|
|
72
|
+
captureRun({ phase: 'spec', domain: 'api', type: 'insight', severity: 'low', summary: 'Second issue', dir });
|
|
73
|
+
|
|
74
|
+
const content = readFileSync(join(dir, 'INDEX.md'), 'utf-8');
|
|
75
|
+
assert.match(content, /First issue/);
|
|
76
|
+
assert.match(content, /Second issue/);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('handles Chinese characters in summary (slugify)', () => {
|
|
80
|
+
const result = captureRun({
|
|
81
|
+
phase: 'review', domain: 'frontend', type: 'pitfall', severity: 'high',
|
|
82
|
+
summary: '中文标题测试特殊字符', dir,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
assert.ok(result.file.includes('review/'), 'file should be in review phase');
|
|
86
|
+
assert.ok(existsSync(join(dir, result.file)), 'file with Chinese slug should exist');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('uses cross-phase as default when no phase specified', () => {
|
|
90
|
+
const result = captureRun({ dir });
|
|
91
|
+
assert.equal(result.phase, 'cross-phase');
|
|
92
|
+
assert.equal(result.domain, 'general');
|
|
93
|
+
assert.equal(result.severity, 'medium');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('rejects invalid phase', () => {
|
|
97
|
+
// capture calls process.exit(1) on invalid phase
|
|
98
|
+
// We test by checking that the function does not create files
|
|
99
|
+
const origExit = process.exit;
|
|
100
|
+
let exitCode = null;
|
|
101
|
+
process.exit = (code) => { exitCode = code; };
|
|
102
|
+
try {
|
|
103
|
+
captureRun({ phase: 'invalid-phase', dir });
|
|
104
|
+
} catch { /* ignore */ }
|
|
105
|
+
process.exit = origExit;
|
|
106
|
+
assert.equal(exitCode, 1, 'should exit with code 1 for invalid phase');
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// tests/lib/solutions-index-gen.test.mjs
|
|
2
|
+
// Tests for scripts/lib/solutions-index-gen.mjs — full INDEX.md rebuild
|
|
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
|
+
const { run: indexGenRun } = await import(join(ROOT, 'scripts', 'lib', 'solutions-index-gen.mjs'));
|
|
16
|
+
|
|
17
|
+
function makeDir() {
|
|
18
|
+
return mkdtempSync(join(tmpdir(), 'tf-sol-indexgen-'));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function cleanup(dir) {
|
|
22
|
+
if (existsSync(dir)) rmSync(dir, { recursive: true, force: true });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function writeEntry(dir, phase, filename, fm) {
|
|
26
|
+
const phaseDir = join(dir, phase);
|
|
27
|
+
mkdirSync(phaseDir, { recursive: true });
|
|
28
|
+
const fmLines = Object.entries(fm).map(([k, v]) => `${k}: ${v}`).join('\n');
|
|
29
|
+
writeFileSync(join(phaseDir, filename), `---\n${fmLines}\n---\n\n${fm.summary || 'Body text'}\n`, 'utf-8');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('solutions-index-gen', () => {
|
|
33
|
+
let dir;
|
|
34
|
+
|
|
35
|
+
beforeEach(() => { dir = makeDir(); });
|
|
36
|
+
after(() => { cleanup(dir); });
|
|
37
|
+
|
|
38
|
+
it('rebuilds INDEX.md from phase directories', () => {
|
|
39
|
+
writeEntry(dir, 'prd', '2026-07-01-auth-issue.md', {
|
|
40
|
+
phase: 'prd', domain: 'auth', type: 'pitfall', severity: 'high', date: '2026-07-01',
|
|
41
|
+
});
|
|
42
|
+
writeEntry(dir, 'build', '2026-07-02-ci-pattern.md', {
|
|
43
|
+
phase: 'build', domain: 'ci', type: 'pattern', severity: 'medium', date: '2026-07-02',
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
indexGenRun({ dir });
|
|
47
|
+
|
|
48
|
+
const content = readFileSync(join(dir, 'INDEX.md'), 'utf-8');
|
|
49
|
+
assert.match(content, /# Solutions Index/);
|
|
50
|
+
assert.match(content, /auth-issue/);
|
|
51
|
+
assert.match(content, /ci-pattern/);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('sorts by severity descending (high before medium before low)', () => {
|
|
55
|
+
writeEntry(dir, 'prd', 'low.md', {
|
|
56
|
+
phase: 'prd', domain: 'd1', type: 'pitfall', severity: 'low', date: '2026-07-03',
|
|
57
|
+
});
|
|
58
|
+
writeEntry(dir, 'prd', 'high.md', {
|
|
59
|
+
phase: 'prd', domain: 'd2', type: 'pitfall', severity: 'high', date: '2026-07-01',
|
|
60
|
+
});
|
|
61
|
+
writeEntry(dir, 'prd', 'medium.md', {
|
|
62
|
+
phase: 'prd', domain: 'd3', type: 'pitfall', severity: 'medium', date: '2026-07-02',
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
indexGenRun({ dir });
|
|
66
|
+
|
|
67
|
+
const content = readFileSync(join(dir, 'INDEX.md'), 'utf-8');
|
|
68
|
+
const highPos = content.indexOf('d2');
|
|
69
|
+
const medPos = content.indexOf('d3');
|
|
70
|
+
const lowPos = content.indexOf('d1');
|
|
71
|
+
assert.ok(highPos < medPos, 'high should appear before medium');
|
|
72
|
+
assert.ok(medPos < lowPos, 'medium should appear before low');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('sorts by date descending within same severity', () => {
|
|
76
|
+
writeEntry(dir, 'prd', 'older.md', {
|
|
77
|
+
phase: 'prd', domain: 'auth', type: 'pitfall', severity: 'high', date: '2026-07-01',
|
|
78
|
+
});
|
|
79
|
+
writeEntry(dir, 'prd', 'newer.md', {
|
|
80
|
+
phase: 'prd', domain: 'auth', type: 'pitfall', severity: 'high', date: '2026-07-15',
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
indexGenRun({ dir });
|
|
84
|
+
|
|
85
|
+
const content = readFileSync(join(dir, 'INDEX.md'), 'utf-8');
|
|
86
|
+
const newerPos = content.indexOf('2026-07-15');
|
|
87
|
+
const olderPos = content.indexOf('2026-07-01');
|
|
88
|
+
assert.ok(newerPos < olderPos, 'newer date should appear before older date');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('scans all 7 phase directories', () => {
|
|
92
|
+
const phases = ['prd', 'plan', 'prototype', 'spec', 'build', 'review', 'cross-phase'];
|
|
93
|
+
for (const phase of phases) {
|
|
94
|
+
writeEntry(dir, phase, `2026-07-01-${phase}-entry.md`, {
|
|
95
|
+
phase, domain: 'general', type: 'insight', severity: 'medium', date: '2026-07-01',
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
indexGenRun({ dir });
|
|
100
|
+
|
|
101
|
+
const content = readFileSync(join(dir, 'INDEX.md'), 'utf-8');
|
|
102
|
+
for (const phase of phases) {
|
|
103
|
+
assert.match(content, new RegExp(phase), `INDEX should contain ${phase} entries`);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('skips non-.md files', () => {
|
|
108
|
+
mkdirSync(join(dir, 'build'), { recursive: true });
|
|
109
|
+
writeFileSync(join(dir, 'build', 'readme.txt'), 'Not a markdown file', 'utf-8');
|
|
110
|
+
writeEntry(dir, 'build', '2026-07-01-real.md', {
|
|
111
|
+
phase: 'build', domain: 'ci', type: 'pattern', severity: 'high', date: '2026-07-01',
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
indexGenRun({ dir });
|
|
115
|
+
|
|
116
|
+
const content = readFileSync(join(dir, 'INDEX.md'), 'utf-8');
|
|
117
|
+
assert.doesNotMatch(content, /readme\.txt/);
|
|
118
|
+
assert.match(content, /real\.md/);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('truncates to 150 entries (drops lowest priority)', () => {
|
|
122
|
+
// Create 155 entries (all high severity, varying dates)
|
|
123
|
+
mkdirSync(join(dir, 'build'), { recursive: true });
|
|
124
|
+
for (let i = 0; i < 155; i++) {
|
|
125
|
+
const day = String((i % 28) + 1).padStart(2, '0');
|
|
126
|
+
writeEntry(dir, 'build', `entry-${String(i).padStart(3, '0')}.md`, {
|
|
127
|
+
phase: 'build', domain: `d${i}`, type: 'pattern', severity: 'high', date: `2026-07-${day}`,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
indexGenRun({ dir });
|
|
132
|
+
|
|
133
|
+
const content = readFileSync(join(dir, 'INDEX.md'), 'utf-8');
|
|
134
|
+
const dataLines = content.split('\n').filter(l => l.startsWith('|') && !l.startsWith('| date') && !l.startsWith('|--'));
|
|
135
|
+
assert.equal(dataLines.length, 150, 'INDEX should be truncated to 150 entries');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('handles empty solutions directory', () => {
|
|
139
|
+
// No phase directories exist
|
|
140
|
+
indexGenRun({ dir });
|
|
141
|
+
|
|
142
|
+
const content = readFileSync(join(dir, 'INDEX.md'), 'utf-8');
|
|
143
|
+
assert.match(content, /# Solutions Index/);
|
|
144
|
+
const dataLines = content.split('\n').filter(l => l.startsWith('|') && !l.startsWith('| date') && !l.startsWith('|--'));
|
|
145
|
+
assert.equal(dataLines.length, 0);
|
|
146
|
+
});
|
|
147
|
+
});
|