@xulthekl/team-flow 0.63.0 → 0.64.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/.github/workflows/ci.yml +2 -0
- package/CHANGELOG.md +28 -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/state-machine.md +4 -1
- package/docs/team-flow /344/275/277/347/224/250/350/257/264/346/230/216/357/274/210/347/240/224/345/217/221/345/233/242/351/230/237/347/211/210/357/274/211.md" +2 -2
- package/gemini-extension.json +1 -1
- 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/_fs-utils.mjs +18 -0
- package/scripts/guard/checks/arch-design-light.mjs +39 -0
- package/scripts/guard/checks/arch-merged-light.mjs +67 -0
- package/scripts/guard/checks/arch-snapshot-light.mjs +30 -0
- package/scripts/guard/checks/artifacts-planned.mjs +38 -0
- package/scripts/guard/checks/compound-writeback-light.mjs +45 -0
- package/scripts/guard/checks/cross-change-consistency-light.mjs +75 -0
- package/scripts/guard/checks/direct-short-path.mjs +52 -0
- package/scripts/guard/checks/direct-test-result.mjs +30 -0
- package/scripts/guard/checks/execution-plan-ready.mjs +7 -1
- package/scripts/guard/checks/execution-reviews-passed-light.mjs +28 -0
- package/scripts/guard/checks/lightweight-completion-evidence.mjs +27 -0
- package/scripts/guard/checks/specs-merged.mjs +25 -1
- package/scripts/guard/checks/test-matrix-complete.mjs +27 -1
- package/scripts/guard/checks/test-matrix-ready.mjs +28 -1
- package/scripts/guard/checks/test-merged-light.mjs +36 -0
- package/scripts/guard/guard.mjs +102 -12
- package/scripts/infer-workflow.mjs +35 -4
- package/scripts/lib/arch-merge.mjs +20 -4
- package/scripts/lib/cmd-execution.mjs +44 -1
- package/scripts/lib/cmd-state.mjs +94 -4
- package/scripts/lib/execution-plan.mjs +3 -1
- package/scripts/lib/state-loader.mjs +43 -0
- package/scripts/lib/surface-scan.mjs +156 -0
- package/scripts/lib/test-merge.mjs +10 -2
- package/scripts/team-flow.mjs +3 -3
- package/skills/clean-code/SKILL.md +1 -1
- package/skills/jarvis/SKILL.md +2 -0
- package/skills/release-archivist/SKILL.md +39 -13
- package/skills/session-handoff/SKILL.md +1 -0
- package/skills/test-strategy/SKILL.md +1 -1
- package/skills/workflow-start/SKILL.md +63 -5
- package/skills/workflow-start/references/routing-rules.md +4 -4
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { parseArgs } from 'node:util';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
2
4
|
import { createPlan, describeWaves, EXECUTION_MODES, readPlan, recordReview, refreshPlanHash, validatePlan, writePlan } from './execution-plan.mjs';
|
|
3
5
|
import {
|
|
4
6
|
createRecommendationReceipt,
|
|
@@ -6,6 +8,7 @@ import {
|
|
|
6
8
|
writeRecommendationReceipt,
|
|
7
9
|
} from './execution-recommendation.mjs';
|
|
8
10
|
import { readState, writeState } from './state-loader.mjs';
|
|
11
|
+
import { parseTaskLine } from './md-normalize.mjs';
|
|
9
12
|
|
|
10
13
|
const SUBCOMMANDS = ['recommend', 'plan', 'show', 'revise', 'review', 'refresh-hash'];
|
|
11
14
|
|
|
@@ -17,6 +20,8 @@ export async function run(args) {
|
|
|
17
20
|
reason: { type: 'string' },
|
|
18
21
|
wave: { type: 'string', multiple: true },
|
|
19
22
|
confirm: { type: 'boolean', default: false },
|
|
23
|
+
// v0.64.0(实施计划 D7):planned 前门的 tasks.md 派生通道——跳过 recommend/receipt 仪式
|
|
24
|
+
derive: { type: 'boolean', default: false },
|
|
20
25
|
'acknowledge-recommendation': { type: 'boolean', default: false },
|
|
21
26
|
base: { type: 'string' },
|
|
22
27
|
head: { type: 'string' },
|
|
@@ -58,10 +63,47 @@ export async function run(args) {
|
|
|
58
63
|
}
|
|
59
64
|
}
|
|
60
65
|
|
|
66
|
+
// v0.64.0(D7 派生):tasks.md checkbox → 单 wave(serial),仅 planned --derive 通道使用
|
|
67
|
+
function deriveWavesFromTasks(changeDir) {
|
|
68
|
+
const tasksPath = join(changeDir, 'tasks.md');
|
|
69
|
+
if (!existsSync(tasksPath)) throw new Error('--derive requires tasks.md (planned front door: proposal.md + tasks.md)');
|
|
70
|
+
const tasks = readFileSync(tasksPath, 'utf-8')
|
|
71
|
+
.split('\n')
|
|
72
|
+
.map(line => parseTaskLine(line)?.text)
|
|
73
|
+
.map(t => (typeof t === 'string' ? t.trim() : ''))
|
|
74
|
+
.filter(Boolean);
|
|
75
|
+
if (tasks.length === 0) throw new Error('tasks.md has no checkboxes to derive a plan from');
|
|
76
|
+
return [{ id: 'w1', strategy: 'serial', tasks, depends_on: [] }];
|
|
77
|
+
}
|
|
78
|
+
|
|
61
79
|
function createAndPrintPlan(changeDir, values, revise) {
|
|
62
|
-
requireMode(values.mode);
|
|
63
80
|
requireOption(values.reason, '--reason');
|
|
64
81
|
requireSafeReason(values.reason);
|
|
82
|
+
// planned 派生通道(v0.64.0 D7):无 recommendation/receipt/selection——validatePlan 对
|
|
83
|
+
// workflow_variant=planned 豁免三件要求;state.execution_* 由 writeExecutionSummary 同步。
|
|
84
|
+
if (values.derive) {
|
|
85
|
+
if (revise) throw new Error('--derive creates the initial plan; use "tf execution revise" for revisions');
|
|
86
|
+
const st = readState(changeDir);
|
|
87
|
+
if (st.workflow_variant !== 'planned') {
|
|
88
|
+
throw new Error('--derive is only valid for the planned front door (workflow_variant=planned)');
|
|
89
|
+
}
|
|
90
|
+
if (!values.confirm) throw new Error('--derive requires --confirm');
|
|
91
|
+
const mode = values.mode || 'inline';
|
|
92
|
+
requireMode(mode);
|
|
93
|
+
const waves = values.wave?.length ? parseWaves(values.wave) : deriveWavesFromTasks(changeDir);
|
|
94
|
+
const plan = createPlan(changeDir, {
|
|
95
|
+
mode,
|
|
96
|
+
source: 'tasks-derived',
|
|
97
|
+
rationale: values.reason,
|
|
98
|
+
waves,
|
|
99
|
+
});
|
|
100
|
+
const saved = writePlan(changeDir, plan);
|
|
101
|
+
writeExecutionSummary(changeDir, saved);
|
|
102
|
+
print(values.json, { ok: true, plan: saved },
|
|
103
|
+
`Execution plan revision ${saved.revision} recorded (${saved.mode}, derived from tasks.md).`);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
requireMode(values.mode);
|
|
65
107
|
const waves = parseWaves(values.wave);
|
|
66
108
|
const existing = readPlan(changeDir);
|
|
67
109
|
if (revise) {
|
|
@@ -236,6 +278,7 @@ function printHelp() {
|
|
|
236
278
|
console.log(`Usage:
|
|
237
279
|
tf execution recommend <dir> [--wave <id>:<strategy>:<task,...>[:<depends-on,...>]] [--json]
|
|
238
280
|
tf execution plan <dir> --mode <mode> --confirm --reason <text> --wave <id>:<strategy>:<task,...>[:<depends-on,...>] [--acknowledge-recommendation]
|
|
281
|
+
tf execution plan <dir> --derive --confirm --reason <text> [--mode <mode>] # planned only: derive single wave from tasks.md
|
|
239
282
|
tf execution show <dir> [--json]
|
|
240
283
|
tf execution revise <dir> --mode sdd --confirm --reason <text> --wave <id>:<strategy>:<task,...>[:<depends-on,...>] [--acknowledge-recommendation]
|
|
241
284
|
tf execution review <dir> --wave <id> --base <sha> --head <sha> --report <path> --verdict pass|fail [--repo <path>] [--tests-total N --tests-passed N --tests-failed N]
|
|
@@ -17,6 +17,13 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
17
17
|
// schema_version 不可设置(仅 init 打戳,防止伪造豁免身份,v0.13 §48.1)。
|
|
18
18
|
const SETTABLE_FIELDS = [
|
|
19
19
|
'workflow', 'batches_completed', 'spec_merged',
|
|
20
|
+
// v0.64.0 P0(实施计划 §3.3):前门双维度。三环齐全(本列表 + state-loader
|
|
21
|
+
// BUILTIN_DEFAULTS + writeState 序列化分支),缺一即「回显成功却零写入」。
|
|
22
|
+
// workflow_variant 走通用通道仅 exploring 态可写(下方 set 分支强制);
|
|
23
|
+
// 升档豁免 = tf state upgrade 专用子命令(不经本通道),写入打
|
|
24
|
+
// variant_source=upgrade + variant_direction=up,guard 据此 fail-closed。
|
|
25
|
+
'workflow_variant', 'variant_source', 'variant_direction',
|
|
26
|
+
'planned_arch', 'model_profile',
|
|
20
27
|
'dp_0_decisions', 'dp_0_confirmed', 'dp_0_timestamp', 'dp_0_result',
|
|
21
28
|
// v0.59.0(P4 实测):dp_{1,2,3,5,6,7}_decisions / _confirmed 共 12 个字段已移除——
|
|
22
29
|
// 它们虽在旧白名单内,但 writeState 无对应序列化分支,tf state set 会**回显成功却零写入**
|
|
@@ -49,6 +56,8 @@ const SETTABLE_FIELDS = [
|
|
|
49
56
|
// 三键必须与 state-loader BUILTIN_DEFAULTS + writeState 序列化分支同步注册,
|
|
50
57
|
// 缺任一环即「回显成功却零写入」(v0.59.0 同型教训,见本文件 :20-24 注释)。
|
|
51
58
|
'gates_probed_skipped', 'gates_probed_skip_reason', 'gates_probed_na',
|
|
59
|
+
// v0.64.0(§4 arch-design-light):轻架构说明显式跳过键(须附理由)
|
|
60
|
+
'arch_design_light_skipped', 'arch_design_light_skip_reason',
|
|
52
61
|
];
|
|
53
62
|
|
|
54
63
|
export async function run(args) {
|
|
@@ -66,7 +75,7 @@ export async function run(args) {
|
|
|
66
75
|
|
|
67
76
|
if (!changeDir) {
|
|
68
77
|
console.error('Usage: tf state <subcommand> <change-dir> [arg]');
|
|
69
|
-
console.error('Subcommands: init, check, transition, get, rebuild, set');
|
|
78
|
+
console.error('Subcommands: init, check, transition, get, rebuild, set, upgrade');
|
|
70
79
|
process.exit(2);
|
|
71
80
|
}
|
|
72
81
|
|
|
@@ -80,7 +89,7 @@ export async function run(args) {
|
|
|
80
89
|
// Unknown subcommand: report a usage error (exit 2) BEFORE the BUG-B
|
|
81
90
|
// state-file existence check, so a bad subcommand is not masked by the
|
|
82
91
|
// "No state file" error (which would return exit 1 instead of exit 2).
|
|
83
|
-
const KNOWN_SUBS = ['init', 'check', 'transition', 'get', 'rebuild', 'set'];
|
|
92
|
+
const KNOWN_SUBS = ['init', 'check', 'transition', 'get', 'rebuild', 'set', 'upgrade'];
|
|
84
93
|
if (!KNOWN_SUBS.includes(sub)) {
|
|
85
94
|
console.error(`Unknown subcommand: ${sub}. Valid: ${KNOWN_SUBS.join(', ')}`);
|
|
86
95
|
process.exit(2);
|
|
@@ -112,6 +121,12 @@ export async function run(args) {
|
|
|
112
121
|
const state = readState(changeDir);
|
|
113
122
|
if (!stateFileExisted) {
|
|
114
123
|
state.schema_version = 1;
|
|
124
|
+
// v0.64.0 P0(§4 扫描基线):init 打戳 base_sha(架构 surface 扫描的 git 基线)。
|
|
125
|
+
// 非 git 环境 / 尚无提交 → 保持 null,扫描时按 §4 退化参照(origin → FAIL)。
|
|
126
|
+
try {
|
|
127
|
+
const rev = spawnSync('git', ['rev-parse', 'HEAD'], { cwd: changeDir, encoding: 'utf-8', timeout: 3_000 });
|
|
128
|
+
if (rev.status === 0 && rev.stdout?.trim()) state.base_sha = rev.stdout.trim();
|
|
129
|
+
} catch { /* 保持 null */ }
|
|
115
130
|
}
|
|
116
131
|
state.artifacts_hash = hash;
|
|
117
132
|
state.contract_hash = ch;
|
|
@@ -170,7 +185,16 @@ export async function run(args) {
|
|
|
170
185
|
const rawWorkflow = state.workflow || 'full';
|
|
171
186
|
// Normalize: guard only accepts full/hotfix/tweak, not "auto"
|
|
172
187
|
const workflow = rawWorkflow === 'auto' ? 'full' : rawWorkflow;
|
|
173
|
-
|
|
188
|
+
// v0.64.0 P1(§3.3/§4.5 注记①):透传前门变体——guard 据此选 quick/planned 维度表。
|
|
189
|
+
// 这是 workflow_variant 到达 resolveDimensions 的唯一管道(workflow-start 是 skill,不是调用方)。
|
|
190
|
+
const guardArgs = [guardScript, 'check', changeDir, fromState, toState, '--json', '--workflow', workflow];
|
|
191
|
+
if (state.workflow_variant != null && state.workflow_variant !== 'null') {
|
|
192
|
+
guardArgs.push('--workflow-variant', String(state.workflow_variant));
|
|
193
|
+
}
|
|
194
|
+
if (state.planned_arch === true || state.planned_arch === 'true') {
|
|
195
|
+
guardArgs.push('--planned-arch', 'true');
|
|
196
|
+
}
|
|
197
|
+
const guardResult = spawnSync('node', guardArgs, {
|
|
174
198
|
cwd: join(__dirname, '..', '..'),
|
|
175
199
|
timeout: 10_000,
|
|
176
200
|
});
|
|
@@ -282,6 +306,28 @@ export async function run(args) {
|
|
|
282
306
|
}
|
|
283
307
|
process.exit(1);
|
|
284
308
|
}
|
|
309
|
+
// v0.64.0 P0 写限(§3.3/§5.3):workflow_variant 通用通道仅 exploring 态可写,
|
|
310
|
+
// 防关门段自降档清屏;升档唯一豁免 = tf state upgrade 专用子命令(不走本通道)。
|
|
311
|
+
if (field === 'workflow_variant') {
|
|
312
|
+
const cur = readState(changeDir);
|
|
313
|
+
if (cur.state !== 'exploring') {
|
|
314
|
+
console.error(
|
|
315
|
+
`⛔ 'workflow_variant' 仅可在 exploring 态通过本通道写入(当前: ${cur.state})。\n` +
|
|
316
|
+
` 升档唯一机械入口: tf state upgrade <change-dir> <planned|full>(原子改写 variant_source=upgrade + 回退 approved-for-build + 重算 hash;降档拒绝)`
|
|
317
|
+
);
|
|
318
|
+
process.exit(1);
|
|
319
|
+
}
|
|
320
|
+
if (value !== 'null' && !['direct', 'planned', 'legacy'].includes(value)) {
|
|
321
|
+
console.error(`⛔ Invalid workflow_variant: '${value}'. Must be one of: direct, planned, legacy, null`);
|
|
322
|
+
process.exit(1);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (field === 'model_profile') {
|
|
326
|
+
if (!['mechanical', 'standard', 'strong', 'review'].includes(value)) {
|
|
327
|
+
console.error(`⛔ Invalid model_profile: '${value}'. Must be one of: mechanical, standard, strong, review`);
|
|
328
|
+
process.exit(1);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
285
331
|
updateField(changeDir, field, value);
|
|
286
332
|
if (values.json) {
|
|
287
333
|
console.log(JSON.stringify({ ok: true, field, value }));
|
|
@@ -290,8 +336,52 @@ export async function run(args) {
|
|
|
290
336
|
}
|
|
291
337
|
break;
|
|
292
338
|
}
|
|
339
|
+
case 'upgrade': {
|
|
340
|
+
// v0.64.0(实施计划 §5.3 D9,升档唯一机械入口):
|
|
341
|
+
// tf state upgrade <change-dir> <planned|full>
|
|
342
|
+
// 校验方向向上 → 原子写 variant_source=upgrade + variant_direction=up →
|
|
343
|
+
// state 回退 approved-for-build(重跑目标档闸门)→ 重算 artifacts_hash(B-10)。
|
|
344
|
+
// 保留:磁盘代码/产物、test_result + test_evidence_path、test_matrix_hash。
|
|
345
|
+
const toVariant = arg;
|
|
346
|
+
if (!['planned', 'full'].includes(toVariant)) {
|
|
347
|
+
console.error('Usage: tf state upgrade <change-dir> <planned|full> (downgrades are forbidden)');
|
|
348
|
+
process.exit(2);
|
|
349
|
+
}
|
|
350
|
+
const ORDER = { direct: 0, planned: 1 };
|
|
351
|
+
const cur = readState(changeDir);
|
|
352
|
+
const from = cur.workflow_variant;
|
|
353
|
+
// 档位序:direct(0) < planned(1) < full/legacy(2)。upgrade full = 置 legacy + workflow full
|
|
354
|
+
//(variant 枚举无 'full',full 档由 workflow=full + legacy 全量仪式表达,方案 §5.3)。
|
|
355
|
+
const fromOrder = from === 'legacy' || from == null ? 2 : (ORDER[from] ?? 0);
|
|
356
|
+
const targetOrder = toVariant === 'full' ? 2 : ORDER[toVariant];
|
|
357
|
+
if (fromOrder >= targetOrder) {
|
|
358
|
+
console.error(`⛔ Upgrade ${from ?? 'null'} -> ${toVariant} is not upward (downgrades are forbidden)`);
|
|
359
|
+
process.exit(1);
|
|
360
|
+
}
|
|
361
|
+
cur.workflow_variant = toVariant === 'full' ? 'legacy' : toVariant;
|
|
362
|
+
cur.variant_source = 'upgrade';
|
|
363
|
+
cur.variant_direction = 'up';
|
|
364
|
+
if (toVariant === 'full') cur.workflow = 'full';
|
|
365
|
+
// 已过规划段则回退 approved-for-build 重跑目标档闸门;exploring 态无需回退
|
|
366
|
+
const ROLLBACK_STATES = ['specifying', 'bridging', 'executing', 'debugging', 'closing'];
|
|
367
|
+
if (ROLLBACK_STATES.includes(cur.state)) {
|
|
368
|
+
const prev = cur.state;
|
|
369
|
+
cur.state = 'approved-for-build';
|
|
370
|
+
cur.last_transition_from = prev;
|
|
371
|
+
cur.last_transition_to = 'approved-for-build';
|
|
372
|
+
cur.last_transition = new Date().toISOString();
|
|
373
|
+
}
|
|
374
|
+
cur.artifacts_hash = computeArtifactsHash(changeDir); // B-10 重算,防 doctor/check 恒报不一致
|
|
375
|
+
writeState(changeDir, cur);
|
|
376
|
+
if (values.json) {
|
|
377
|
+
console.log(JSON.stringify({ ok: true, from: from ?? 'null', to: cur.workflow_variant, state: cur.state, variant_source: 'upgrade' }));
|
|
378
|
+
} else {
|
|
379
|
+
console.log(`✅ Upgraded workflow_variant: ${from ?? 'null'} -> ${cur.workflow_variant} (source=upgrade, state=${cur.state}, artifacts_hash recomputed)`);
|
|
380
|
+
}
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
293
383
|
default:
|
|
294
|
-
console.error(`Unknown subcommand: ${sub}. Valid: init, check, transition, get, rebuild, set`);
|
|
384
|
+
console.error(`Unknown subcommand: ${sub}. Valid: init, check, transition, get, rebuild, set, upgrade`);
|
|
295
385
|
process.exit(2);
|
|
296
386
|
}
|
|
297
387
|
}
|
|
@@ -107,7 +107,9 @@ export function validatePlan(changeDir, plan) {
|
|
|
107
107
|
if (state.revision != null && plan?.revision !== state.revision) {
|
|
108
108
|
failures.push('execution plan revision does not match state');
|
|
109
109
|
}
|
|
110
|
-
|
|
110
|
+
// v0.64.0(§4.5 注记⑤配套):planned(workflow=full+variant=planned)无 recommendation/
|
|
111
|
+
// selection 仪式——plan 由 tasks.md 派生,不走 execution-recommendation 流程;豁免三件要求。
|
|
112
|
+
if (plan?.workflow !== 'tweak' && state.workflow_variant !== 'planned') {
|
|
111
113
|
if (plan?.recommendation === undefined) failures.push('execution plan recommendation is required for full/hotfix');
|
|
112
114
|
if (plan?.recommendation_receipt === undefined) failures.push('execution plan recommendation receipt is required for full/hotfix');
|
|
113
115
|
if (plan?.selection === undefined) failures.push('execution plan selection is required for full/hotfix');
|
|
@@ -15,6 +15,21 @@ export const VALID_STATES = [
|
|
|
15
15
|
const BUILTIN_DEFAULTS = {
|
|
16
16
|
state: 'exploring',
|
|
17
17
|
workflow: 'auto',
|
|
18
|
+
// spec-superflow 2.0 双维度(v0.64.0 P0;实施计划 §3.1):
|
|
19
|
+
// workflow 仍是内部档位 full|hotfix|tweak|quick|lightweight(+auto 归一化前);
|
|
20
|
+
// workflow_variant 是用户前门意图 null|direct|planned|legacy——两者不可混用。
|
|
21
|
+
// 旧 yaml 缺字段 → null,路由语义 = legacy 分支(见 workflow-start SKILL)。
|
|
22
|
+
workflow_variant: null,
|
|
23
|
+
// 升档标记(§5.3):variant_source ∈ null|start|upgrade;direction ∈ null|up|down。
|
|
24
|
+
// guard fail-closed 校验数据源——非 exploring 改 variant 必须 source=upgrade 且 direction=up。
|
|
25
|
+
variant_source: null,
|
|
26
|
+
variant_direction: null,
|
|
27
|
+
// planned 是否走轻架构表(arch-design-light + arch-snapshot-light,G3)
|
|
28
|
+
planned_arch: false,
|
|
29
|
+
// 角色级模型分配档(config-loader MODEL_PROFILES),仅解析不改流程(§3.5)
|
|
30
|
+
model_profile: 'standard',
|
|
31
|
+
// 架构 surface 扫描基线(§4):tf state init 打戳 git HEAD;缺失时扫描退化参照
|
|
32
|
+
base_sha: null,
|
|
18
33
|
revision: null,
|
|
19
34
|
artifacts_hash: null,
|
|
20
35
|
contract_hash: null,
|
|
@@ -90,6 +105,10 @@ const BUILTIN_DEFAULTS = {
|
|
|
90
105
|
gates_probed_skipped: null,
|
|
91
106
|
gates_probed_skip_reason: null,
|
|
92
107
|
gates_probed_na: null,
|
|
108
|
+
// v0.64.0(§4 arch-design-light):轻架构说明的显式跳过键(skip-with-reason 模式,
|
|
109
|
+
// 与 arch_merge_skipped / test_matrix_skipped 同型:跳过必须附理由留痕)
|
|
110
|
+
arch_design_light_skipped: null,
|
|
111
|
+
arch_design_light_skip_reason: null,
|
|
93
112
|
// 注意:schema_version 故意不在 BUILTIN_DEFAULTS 中(v0.13 §48.1)——
|
|
94
113
|
// 它只由 `tf state init` 在 change 创建时打戳,字段缺失本身就是"存量 change"信号。
|
|
95
114
|
};
|
|
@@ -135,6 +154,26 @@ export function writeState(changeDir, state) {
|
|
|
135
154
|
lines.push('# === Core state ===');
|
|
136
155
|
lines.push(`state: ${state.state || 'exploring'}`);
|
|
137
156
|
lines.push(`workflow: ${state.workflow || 'auto'}`);
|
|
157
|
+
// v0.64.0 P0:前门双维度字段。条件序列化(同 schema_version 模式)——
|
|
158
|
+
// 存量 change 无这些字段时不追加,避免全量 churn;readState 缺失回退 BUILTIN_DEFAULTS。
|
|
159
|
+
if (state.workflow_variant != null) {
|
|
160
|
+
lines.push(`workflow_variant: ${state.workflow_variant}`);
|
|
161
|
+
}
|
|
162
|
+
if (state.variant_source != null) {
|
|
163
|
+
lines.push(`variant_source: ${state.variant_source}`);
|
|
164
|
+
}
|
|
165
|
+
if (state.variant_direction != null) {
|
|
166
|
+
lines.push(`variant_direction: ${state.variant_direction}`);
|
|
167
|
+
}
|
|
168
|
+
if (state.planned_arch === true || state.planned_arch === 'true') {
|
|
169
|
+
lines.push(`planned_arch: true`);
|
|
170
|
+
}
|
|
171
|
+
if (state.model_profile != null && state.model_profile !== 'standard') {
|
|
172
|
+
lines.push(`model_profile: ${state.model_profile}`);
|
|
173
|
+
}
|
|
174
|
+
if (state.base_sha != null) {
|
|
175
|
+
lines.push(`base_sha: ${state.base_sha}`);
|
|
176
|
+
}
|
|
138
177
|
lines.push(`revision: ${state.revision ?? 'null'}`);
|
|
139
178
|
// v0.13 §48.1:schema_version 仅由 tf state init 在 change 创建时打戳;
|
|
140
179
|
// 缺失 = 存量 change(测试门禁豁免键)。rebuild/set 不追加,故条件序列化。
|
|
@@ -225,6 +264,10 @@ export function writeState(changeDir, state) {
|
|
|
225
264
|
lines.push(`gates_probed_skipped: ${state.gates_probed_skipped ?? 'null'}`);
|
|
226
265
|
lines.push(`gates_probed_skip_reason: ${state.gates_probed_skip_reason ?? 'null'}`);
|
|
227
266
|
lines.push(`gates_probed_na: ${state.gates_probed_na ?? 'null'}`);
|
|
267
|
+
lines.push('');
|
|
268
|
+
lines.push('# === Light architecture design gate (v0.64.0) ===');
|
|
269
|
+
lines.push(`arch_design_light_skipped: ${state.arch_design_light_skipped ?? 'null'}`);
|
|
270
|
+
lines.push(`arch_design_light_skip_reason: ${state.arch_design_light_skip_reason ?? 'null'}`);
|
|
228
271
|
|
|
229
272
|
fs.writeFileSync(filePath, lines.join('\n') + '\n', 'utf-8');
|
|
230
273
|
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// scripts/lib/surface-scan.mjs — 架构 surface 扫描共享层(v0.64.0,实施计划 §4)
|
|
2
|
+
//
|
|
3
|
+
// 唯一真相源:direct-short-path / arch-merged-light / test-merged-light /
|
|
4
|
+
// cross-change-consistency-light 都从这里取「本次变更碰了哪些文件、是否命中架构 surface」。
|
|
5
|
+
// 判据统一为「架构 surface = API / DB / 聚合」(R2 A-17 统一,禁止各 check 自造变体)。
|
|
6
|
+
//
|
|
7
|
+
// 三源合并(B-05/B-06):已提交 diff + 工作区修改 + untracked;基线 = state.base_sha,
|
|
8
|
+
// 缺失退化 origin/<default> merge-base,再无 → no-baseline(fail-closed,禁 merge-base HEAD HEAD 空过)。
|
|
9
|
+
// 聚合清单(B-04):.team-flow/aggregate-dirs.txt 机器清单(每行一个目录前缀,# 注释,
|
|
10
|
+
// 空文件 = 显式确认无聚合);与 docs/architecture/baseline.md 皆缺失 → missing(fail-closed)。
|
|
11
|
+
import { execFileSync } from 'node:child_process';
|
|
12
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
13
|
+
import { dirname, join } from 'node:path';
|
|
14
|
+
|
|
15
|
+
const GIT_TIMEOUT_MS = 2_000; // §4 扫描预算 ≤2s(cmd-state spawnSync 10s 总超时)
|
|
16
|
+
|
|
17
|
+
// API/DB surface 路径模式(§4 路径模式表)——聚合只认清单目录,不做词义解析
|
|
18
|
+
const SURFACE_PATH_RE = /(^|\/)(api|apis|routes|controllers?)(\/|$)|mapper|migrations?(\/|$)|(^|\/)schema[._/-]|\.(sql)$/i;
|
|
19
|
+
// 测试文件(test-merged-light 用)
|
|
20
|
+
const TEST_PATH_RE = /(^|\/)(tests?|__tests__|spec)(\/|$)|\.(test|spec)\.[jt]sx?$|Test\.java$/i;
|
|
21
|
+
|
|
22
|
+
function git(args, cwd) {
|
|
23
|
+
return execFileSync('git', args, {
|
|
24
|
+
cwd,
|
|
25
|
+
encoding: 'utf-8',
|
|
26
|
+
timeout: GIT_TIMEOUT_MS,
|
|
27
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
28
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** 由 changes/<name> 推项目根:优先含 changes/ 或 docs/ 的祖先 */
|
|
33
|
+
export function findProjectRoot(changeDir) {
|
|
34
|
+
const a = dirname(dirname(changeDir));
|
|
35
|
+
if (existsSync(join(a, 'changes')) || existsSync(join(a, 'docs'))) return a;
|
|
36
|
+
const b = dirname(changeDir);
|
|
37
|
+
if (existsSync(join(b, 'changes')) || existsSync(join(b, 'docs'))) return b;
|
|
38
|
+
return a;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 解析聚合清单。返回 { missing, dirs[] }。
|
|
43
|
+
* missing=true 表示两处皆无 → 调用方 fail-closed。
|
|
44
|
+
*/
|
|
45
|
+
export function readAggregateList(projectRoot) {
|
|
46
|
+
const txt = join(projectRoot, '.team-flow', 'aggregate-dirs.txt');
|
|
47
|
+
const baseline = join(projectRoot, 'docs', 'architecture', 'baseline.md');
|
|
48
|
+
const txtExists = existsSync(txt);
|
|
49
|
+
const baselineExists = existsSync(baseline);
|
|
50
|
+
if (!txtExists && !baselineExists) return { missing: true, dirs: [] };
|
|
51
|
+
if (!txtExists) return { missing: false, dirs: [] }; // 有 baseline 但无机器清单:清单维度放行,聚合匹配按空表(见设计 §4)
|
|
52
|
+
const dirs = readFileSync(txt, 'utf-8')
|
|
53
|
+
.split('\n')
|
|
54
|
+
.map(l => l.trim())
|
|
55
|
+
.filter(l => l && !l.startsWith('#'));
|
|
56
|
+
return { missing: false, dirs };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function readScanIgnore(projectRoot) {
|
|
60
|
+
const p = join(projectRoot, '.team-flow', 'scan-ignore');
|
|
61
|
+
if (!existsSync(p)) return [];
|
|
62
|
+
return readFileSync(p, 'utf-8')
|
|
63
|
+
.split('\n')
|
|
64
|
+
.map(l => l.trim())
|
|
65
|
+
.filter(l => l && !l.startsWith('#'));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 三源合并的变更文件清单。
|
|
70
|
+
* 返回 { error: null|'no-baseline'|'diff-failed', files: string[], base }
|
|
71
|
+
*/
|
|
72
|
+
export function gitChangedFiles(changeDir, state) {
|
|
73
|
+
const files = new Set();
|
|
74
|
+
let base = state?.base_sha || null;
|
|
75
|
+
|
|
76
|
+
// 退化:base_sha 缺失 → origin/<default> merge-base(B-05:参照对象写死)
|
|
77
|
+
if (!base) {
|
|
78
|
+
try {
|
|
79
|
+
const ref = git(['symbolic-ref', 'refs/remotes/origin/HEAD'], changeDir).trim();
|
|
80
|
+
const remoteBranch = ref.replace('refs/remotes/', '');
|
|
81
|
+
base = git(['merge-base', 'HEAD', remoteBranch], changeDir).trim();
|
|
82
|
+
} catch {
|
|
83
|
+
return { error: 'no-baseline', files: [], base: null };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 源①:已提交 diff(base...HEAD)
|
|
88
|
+
try {
|
|
89
|
+
for (const f of git(['diff', '--name-only', `${base}...HEAD`], changeDir).split('\n')) {
|
|
90
|
+
if (f.trim()) files.add(f.trim());
|
|
91
|
+
}
|
|
92
|
+
} catch {
|
|
93
|
+
return { error: 'diff-failed', files: [], base };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 源②:工作区已修改
|
|
97
|
+
try {
|
|
98
|
+
for (const f of git(['diff', '--name-only', 'HEAD'], changeDir).split('\n')) {
|
|
99
|
+
if (f.trim()) files.add(f.trim());
|
|
100
|
+
}
|
|
101
|
+
} catch { /* 无 HEAD 时忽略 */ }
|
|
102
|
+
|
|
103
|
+
// 源③:untracked(porcelain -uall;B-06:新建未 add 的文件只有此源可见)
|
|
104
|
+
try {
|
|
105
|
+
for (const line of git(['status', '--porcelain', '-uall'], changeDir).split('\n')) {
|
|
106
|
+
if (!line) continue;
|
|
107
|
+
let p = line.slice(3);
|
|
108
|
+
const arrow = p.indexOf(' -> ');
|
|
109
|
+
if (arrow !== -1) p = p.slice(arrow + 4); // rename 取新名
|
|
110
|
+
p = p.trim().replace(/^"|"$/g, '');
|
|
111
|
+
if (p) files.add(p);
|
|
112
|
+
}
|
|
113
|
+
} catch { /* 忽略 */ }
|
|
114
|
+
|
|
115
|
+
// 排除清单(B-06:防无关 WIP 误报)
|
|
116
|
+
const ignore = readScanIgnore(findProjectRoot(changeDir));
|
|
117
|
+
const out = [...files].filter(f => !ignore.some(prefix => f.startsWith(prefix)));
|
|
118
|
+
return { error: null, files: out, base };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 文件 → 架构 surface 分类。
|
|
123
|
+
* 返回 { architectureSurface, apiFiles, dbFiles, aggregateFiles, testFiles }
|
|
124
|
+
*/
|
|
125
|
+
export function classifyFiles(files, aggregateDirs) {
|
|
126
|
+
const apiFiles = [];
|
|
127
|
+
const dbFiles = [];
|
|
128
|
+
const aggregateFiles = [];
|
|
129
|
+
const testFiles = [];
|
|
130
|
+
for (const f of files) {
|
|
131
|
+
if (TEST_PATH_RE.test(f)) testFiles.push(f);
|
|
132
|
+
if (SURFACE_PATH_RE.test(f)) {
|
|
133
|
+
apiFiles.push(f);
|
|
134
|
+
if (/migrations?(\/|$)|schema[._/-]|\.(sql)$/i.test(f)) dbFiles.push(f);
|
|
135
|
+
}
|
|
136
|
+
if (aggregateDirs.some(d => {
|
|
137
|
+
const norm = d.replace(/\/+$/, '');
|
|
138
|
+
return f === norm || f.startsWith(`${norm}/`);
|
|
139
|
+
})) {
|
|
140
|
+
aggregateFiles.push(f);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const architectureSurface = [...new Set([...apiFiles, ...aggregateFiles])];
|
|
144
|
+
return { architectureSurface, apiFiles, dbFiles, aggregateFiles, testFiles };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** 便捷组合:扫描 + 清单 + 分类(各 light runner 统一入口) */
|
|
148
|
+
export function scanArchitectureSurface(changeDir, state) {
|
|
149
|
+
const changed = gitChangedFiles(changeDir, state);
|
|
150
|
+
if (changed.error) return { error: changed.error, projectRoot: findProjectRoot(changeDir) };
|
|
151
|
+
const projectRoot = findProjectRoot(changeDir);
|
|
152
|
+
const agg = readAggregateList(projectRoot);
|
|
153
|
+
if (agg.missing) return { error: 'aggregate-list-missing', projectRoot };
|
|
154
|
+
const cls = classifyFiles(changed.files, agg.dirs);
|
|
155
|
+
return { error: null, projectRoot, files: changed.files, ...cls };
|
|
156
|
+
}
|
|
@@ -40,6 +40,8 @@ function parseArgv(argv) {
|
|
|
40
40
|
parsed.projectRoot = argv[++i];
|
|
41
41
|
} else if (argv[i] === '--dry-run') {
|
|
42
42
|
parsed.dryRun = true;
|
|
43
|
+
} else if (argv[i] === '--light') {
|
|
44
|
+
parsed.light = true;
|
|
43
45
|
} else if (!argv[i].startsWith('--')) {
|
|
44
46
|
parsed._.push(argv[i]);
|
|
45
47
|
}
|
|
@@ -584,11 +586,13 @@ export async function run(args) {
|
|
|
584
586
|
async function main(argv, projectRoot) {
|
|
585
587
|
const changeDir = argv._[0];
|
|
586
588
|
const dryRun = argv.dryRun || false;
|
|
589
|
+
const light = argv.light || false; // v0.64.0 §4:planned 轻回写入口(同 rewriteIndex 单写)
|
|
587
590
|
|
|
588
591
|
if (!changeDir) {
|
|
589
|
-
console.error('Usage: tf test-merge <change-dir> [--project-root <path>] [--dry-run]');
|
|
592
|
+
console.error('Usage: tf test-merge <change-dir> [--project-root <path>] [--dry-run] [--light]');
|
|
590
593
|
process.exit(2);
|
|
591
594
|
}
|
|
595
|
+
if (light) console.log(' Mode: LIGHT(增量条目 + change:<name> 归因锚写入 changelog)');
|
|
592
596
|
|
|
593
597
|
const absChangeDir = resolve(changeDir);
|
|
594
598
|
const changeName = extractChangeName(absChangeDir);
|
|
@@ -643,7 +647,11 @@ async function main(argv, projectRoot) {
|
|
|
643
647
|
console.log(`${mark} Step 3: resolveDeferred — ${deferredResult.resolved} items resolved`);
|
|
644
648
|
|
|
645
649
|
// Step 4: appendChangelog
|
|
646
|
-
|
|
650
|
+
// v0.64.0 §4:light 模式在 changelog 首行写入归因锚 `change:<name>`——
|
|
651
|
+
// arch-merged-light 同型的 guard 归因判据(test-merged-light)依赖该锚,
|
|
652
|
+
// full 模式文件名即含 changeName,锚为显式加固不改变既有内容。
|
|
653
|
+
const changelogContent = light ? `<!-- change:${changeName} -->\n${matrixContent}` : matrixContent;
|
|
654
|
+
const changelogPath = appendChangelog(ledgerDir, changeName, changelogContent, dryRun);
|
|
647
655
|
console.log(`${mark} Step 4: appendChangelog — ${relative(projectRoot, changelogPath)}`);
|
|
648
656
|
|
|
649
657
|
// Step 5: rewriteIndex
|
package/scripts/team-flow.mjs
CHANGED
|
@@ -71,9 +71,9 @@ Commands:
|
|
|
71
71
|
arch scaffold Scaffold global docs/architecture/ ledger in generator format (v0.53.0 §102)
|
|
72
72
|
arch precheck <change-dir> [--json]
|
|
73
73
|
Emit deterministic architecture-gate evidence (v0.22 §88; evidence only, exit 0)
|
|
74
|
-
arch-merge <change-dir> [--project-root <path>] [--dry-run]
|
|
74
|
+
arch-merge <change-dir> [--project-root <path>] [--dry-run] [--light]
|
|
75
75
|
Merge architecture delta into global docs/architecture/
|
|
76
|
-
test-merge <change-dir> [--project-root <path>] [--dry-run]
|
|
76
|
+
test-merge <change-dir> [--project-root <path>] [--dry-run] [--light]
|
|
77
77
|
Merge test matrix results into global docs/test-ledger/
|
|
78
78
|
test-matrix-export <input.json> <output.md> [--change-id <id>]
|
|
79
79
|
Convert glaf4 test-matrix.json to team-flow test-matrix.md
|
|
@@ -90,7 +90,7 @@ Commands:
|
|
|
90
90
|
pytest: terminal summary or junit XML (--junitxml)
|
|
91
91
|
config [options] Display or modify configuration
|
|
92
92
|
config --resolve-model <profile> Resolve a configured model profile without switching models
|
|
93
|
-
state <sub> <dir> Manage .team-flow.yaml state (init|check|transition|get|rebuild)
|
|
93
|
+
state <sub> <dir> Manage .team-flow.yaml state (init|check|transition|get|rebuild|set|upgrade)
|
|
94
94
|
inject <dir> Generate phase-guard artifacts; use --platforms <name|all> when platform is ambiguous
|
|
95
95
|
audit <dir> Generate decision-point-audit.md from .team-flow.yaml
|
|
96
96
|
checkpoint save <change-dir> --task <id> --next <text>
|
package/skills/jarvis/SKILL.md
CHANGED
|
@@ -16,6 +16,8 @@ description: This skill should be used when the user asks to "启动 Jarvis", "
|
|
|
16
16
|
|
|
17
17
|
**核心边界**:Jarvis 只做决策与编排——**不写产线代码、不改 team-flow 状态文件(`.team-flow.yaml`)、不发明任务方向**。(「不改状态文件」专指 team-flow 的门禁真相源;Jarvis 自己的 `state/*.log|md` 当然要写。)
|
|
18
18
|
|
|
19
|
+
**前门回显(v0.64.0)**:接管/恢复任何 team-flow change 时,简报须读取并回显 `workflow_variant`(direct/planned/legacy/null)——direct/planned **轻路径无 DP-0~7 决策点可代答**(代答权限表对该两档恒 N/A);升档决策(`tf state upgrade`)属「挂起交 LT」类,不在代答权限内。
|
|
20
|
+
|
|
19
21
|
> **与 firstmate 的分工**:Jarvis 是 **team-flow 专用**的决策代理,深度集成状态机 / 决策点 / 门禁;通用跨项目的「大副」角色由 firstmate 承担。两者不重叠——Jarvis 不做通用编码、调查或审计工作。
|
|
20
22
|
|
|
21
23
|
> **命令形态以官方为准**:本文命令随 Orca 版本演进,**运行前先读一次** `orca skills get orchestration`,不要凭记忆推断子命令或参数——v0.60.0 实测暴露的 skill 缺陷之一即命令形态错误。
|