@joekytc/dsh-swarm 0.1.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/LICENSE +21 -0
- package/README.md +504 -0
- package/README.zh-CN.md +452 -0
- package/client/BoardCard.tsx +41 -0
- package/client/ConnectionBanner.tsx +16 -0
- package/client/KanbanBoard.tsx +168 -0
- package/client/KanbanTab.tsx +26 -0
- package/client/RenameModal.tsx +38 -0
- package/client/TaskDrawer.tsx +212 -0
- package/client/WorkflowRail.tsx +175 -0
- package/client/board-store.ts +196 -0
- package/client/css.d.ts +4 -0
- package/client/index.ts +31 -0
- package/client/kanban.css +653 -0
- package/client/useKanbanBoard.ts +7 -0
- package/client/workflow-model.ts +229 -0
- package/cordis.patch.yml +88 -0
- package/lib/client.js +1205 -0
- package/lib/config.d.ts +46 -0
- package/lib/config.js +43 -0
- package/lib/dispatcher/agent-runner.d.ts +23 -0
- package/lib/dispatcher/agent-runner.js +429 -0
- package/lib/dispatcher/chain-auditor.d.ts +47 -0
- package/lib/dispatcher/chain-auditor.js +194 -0
- package/lib/dispatcher/dispatcher.d.ts +50 -0
- package/lib/dispatcher/dispatcher.js +280 -0
- package/lib/dispatcher/event-waker.d.ts +13 -0
- package/lib/dispatcher/event-waker.js +24 -0
- package/lib/dispatcher/git-credentials.d.ts +33 -0
- package/lib/dispatcher/git-credentials.js +78 -0
- package/lib/dispatcher/merge-gate.d.ts +28 -0
- package/lib/dispatcher/merge-gate.js +74 -0
- package/lib/dispatcher/model-candidates.d.ts +13 -0
- package/lib/dispatcher/model-candidates.js +31 -0
- package/lib/dispatcher/session-events.d.ts +28 -0
- package/lib/dispatcher/session-events.js +33 -0
- package/lib/dispatcher/target-repo.d.ts +15 -0
- package/lib/dispatcher/target-repo.js +42 -0
- package/lib/dispatcher/v-orchestrator.d.ts +74 -0
- package/lib/dispatcher/v-orchestrator.js +452 -0
- package/lib/dispatcher/watchdog.d.ts +15 -0
- package/lib/dispatcher/watchdog.js +30 -0
- package/lib/dispatcher/workspace-attach.d.ts +40 -0
- package/lib/dispatcher/workspace-attach.js +112 -0
- package/lib/domain/delivery-contract.d.ts +18 -0
- package/lib/domain/delivery-contract.js +80 -0
- package/lib/domain/delivery-evidence.d.ts +12 -0
- package/lib/domain/delivery-evidence.js +41 -0
- package/lib/domain/event-store.d.ts +20 -0
- package/lib/domain/event-store.js +53 -0
- package/lib/domain/kanban-service.d.ts +94 -0
- package/lib/domain/kanban-service.js +430 -0
- package/lib/domain/permissions.d.ts +7 -0
- package/lib/domain/permissions.js +54 -0
- package/lib/domain/planning-checklist.d.ts +22 -0
- package/lib/domain/planning-checklist.js +81 -0
- package/lib/domain/prefetch-manifest.d.ts +21 -0
- package/lib/domain/prefetch-manifest.js +73 -0
- package/lib/domain/projection.d.ts +3 -0
- package/lib/domain/projection.js +165 -0
- package/lib/domain/review-evidence.d.ts +13 -0
- package/lib/domain/review-evidence.js +76 -0
- package/lib/domain/state-machine.d.ts +4 -0
- package/lib/domain/state-machine.js +32 -0
- package/lib/domain/task-parents.d.ts +17 -0
- package/lib/domain/task-parents.js +39 -0
- package/lib/domain/tdd-classify.d.ts +5 -0
- package/lib/domain/tdd-classify.js +20 -0
- package/lib/domain/types.d.ts +142 -0
- package/lib/domain/types.js +2 -0
- package/lib/index.d.ts +5 -0
- package/lib/index.js +56 -0
- package/lib/roles/preset-installer.d.ts +8 -0
- package/lib/roles/preset-installer.js +53 -0
- package/lib/roles/toolsets.d.ts +62 -0
- package/lib/roles/toolsets.js +332 -0
- package/lib/roles/wiki-worker.d.ts +20 -0
- package/lib/roles/wiki-worker.js +44 -0
- package/lib/routes/kanban-http.d.ts +6 -0
- package/lib/routes/kanban-http.js +159 -0
- package/lib/routes/kanban-sse.d.ts +8 -0
- package/lib/routes/kanban-sse.js +58 -0
- package/lib/routes/planning-driver.d.ts +16 -0
- package/lib/routes/planning-driver.js +52 -0
- package/lib/routes/prefix-router.d.ts +29 -0
- package/lib/routes/prefix-router.js +28 -0
- package/lib/services/kanban-provider.d.ts +16 -0
- package/lib/services/kanban-provider.js +14 -0
- package/lib/tools/kanban-tools.d.ts +11 -0
- package/lib/tools/kanban-tools.js +169 -0
- package/lib/tools/main-session-tools.d.ts +20 -0
- package/lib/tools/main-session-tools.js +170 -0
- package/lib/tools/planning-tools.d.ts +32 -0
- package/lib/tools/planning-tools.js +102 -0
- package/lib/tools/prefetch-tools.d.ts +5 -0
- package/lib/tools/prefetch-tools.js +59 -0
- package/lib/tools/spec-card-tools.d.ts +4 -0
- package/lib/tools/spec-card-tools.js +75 -0
- package/lib/tools/wiki-tools.d.ts +4 -0
- package/lib/tools/wiki-tools.js +57 -0
- package/lib/wiki/kb-linkage.d.ts +9 -0
- package/lib/wiki/kb-linkage.js +87 -0
- package/lib/wiki/page-path.d.ts +6 -0
- package/lib/wiki/page-path.js +28 -0
- package/lib/wiki/wiki-vault-client.d.ts +28 -0
- package/lib/wiki/wiki-vault-client.js +48 -0
- package/package.json +83 -0
- package/personas/kanban-d/agent.cordis.yml +157 -0
- package/personas/kanban-d/preset.yml +2 -0
- package/personas/kanban-dt/agent.cordis.yml +71 -0
- package/personas/kanban-dt/preset.yml +2 -0
- package/personas/kanban-p/agent.cordis.yml +66 -0
- package/personas/kanban-p/preset.yml +2 -0
- package/personas/kanban-pt/agent.cordis.yml +47 -0
- package/personas/kanban-pt/preset.yml +2 -0
- package/personas/kanban-v/agent.cordis.yml +47 -0
- package/personas/kanban-v/preset.yml +2 -0
- package/personas/kanban-w/agent.cordis.yml +47 -0
- package/personas/kanban-w/preset.yml +2 -0
- package/personas/persona-d.md +26 -0
- package/personas/persona-dt.md +18 -0
- package/personas/persona-p.md +13 -0
- package/personas/persona-pt.md +13 -0
- package/personas/persona-v.md +18 -0
- package/personas/persona-w.md +12 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/** prefetch 子代理结构化输出的 JSON Schema(经 ctx.subagents.start 的 outputSchema 传入,
|
|
2
|
+
* 由 spawn provider 在子代理侧强制校验;assertObjectJsonSchema 强制子集内:
|
|
3
|
+
* type/properties/required/items/enum/additionalProperties)。与 validatePrefetchManifest 同源同语义。 */
|
|
4
|
+
export const PREFETCH_MANIFEST_SCHEMA = {
|
|
5
|
+
type: 'object',
|
|
6
|
+
properties: {
|
|
7
|
+
repo: {
|
|
8
|
+
type: 'object',
|
|
9
|
+
properties: {
|
|
10
|
+
localPath: { type: 'string' },
|
|
11
|
+
remoteUrl: { type: 'string' },
|
|
12
|
+
branch: { type: 'string' },
|
|
13
|
+
dirtyFiles: { type: 'array', items: { type: 'string' } },
|
|
14
|
+
},
|
|
15
|
+
required: ['localPath', 'dirtyFiles'],
|
|
16
|
+
},
|
|
17
|
+
files: {
|
|
18
|
+
type: 'array',
|
|
19
|
+
items: {
|
|
20
|
+
type: 'object',
|
|
21
|
+
properties: {
|
|
22
|
+
path: { type: 'string' },
|
|
23
|
+
expected: { type: 'string', enum: ['exists', 'absent', 'content-hash'] },
|
|
24
|
+
note: { type: 'string' },
|
|
25
|
+
},
|
|
26
|
+
required: ['path', 'expected'],
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
required: ['repo', 'files'],
|
|
31
|
+
};
|
|
32
|
+
const EXPECTED_VALUES = new Set(['exists', 'absent', 'content-hash']);
|
|
33
|
+
/** 需求澄清清单 manifest schema 校验(planning-checklist 复用本 schema):返回错误列表(空数组 = 合法)。 */
|
|
34
|
+
export function validatePrefetchManifest(raw) {
|
|
35
|
+
const errors = [];
|
|
36
|
+
if (typeof raw !== 'object' || raw === null)
|
|
37
|
+
return ['manifest must be an object'];
|
|
38
|
+
const m = raw;
|
|
39
|
+
const repo = m['repo'];
|
|
40
|
+
if (typeof repo !== 'object' || repo === null) {
|
|
41
|
+
errors.push('manifest.repo required');
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
const r = repo;
|
|
45
|
+
if (typeof r['localPath'] !== 'string' || r['localPath'].trim().length === 0) {
|
|
46
|
+
errors.push(`manifest.repo.localPath required (got: ${JSON.stringify(r['localPath'])})`);
|
|
47
|
+
}
|
|
48
|
+
if (!Array.isArray(r['dirtyFiles']))
|
|
49
|
+
errors.push('manifest.repo.dirtyFiles must be an array');
|
|
50
|
+
}
|
|
51
|
+
if (!Array.isArray(m['files'])) {
|
|
52
|
+
errors.push('manifest.files must be an array');
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
for (const f of m['files']) {
|
|
56
|
+
if (typeof f !== 'object' || f === null) {
|
|
57
|
+
errors.push('manifest.files entry must be an object');
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const e = f;
|
|
61
|
+
if (typeof e['path'] !== 'string' || e['path'].trim().length === 0) {
|
|
62
|
+
errors.push(`manifest.files[].path (got: ${JSON.stringify(e['path'])})`);
|
|
63
|
+
}
|
|
64
|
+
if (typeof e['expected'] !== 'string' || !EXPECTED_VALUES.has(e['expected'])) {
|
|
65
|
+
errors.push(`manifest.files[].expected (got: ${JSON.stringify(e['expected'])})`);
|
|
66
|
+
}
|
|
67
|
+
if (e['expected'] === 'content-hash' && (typeof e['note'] !== 'string' || e['note'].trim().length === 0)) {
|
|
68
|
+
errors.push(`manifest.files[].note required for content-hash (got: ${JSON.stringify(e['note'])})`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return errors;
|
|
73
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { transitionChain, transitionSpecCard, transitionTask } from './state-machine.js';
|
|
2
|
+
function empty() {
|
|
3
|
+
return { chains: new Map(), tasks: new Map(), specCards: new Map(), handoffs: new Map(), auditWarnings: new Map(), events: [] };
|
|
4
|
+
}
|
|
5
|
+
export function applyTo(state, ev) {
|
|
6
|
+
const next = { ...state, events: [...state.events, ev] };
|
|
7
|
+
switch (ev.kind) {
|
|
8
|
+
case 'chain/created': {
|
|
9
|
+
const p = ev.payload;
|
|
10
|
+
next.chains = new Map(state.chains).set(p.id, { ...p, status: 'planning', workspaceDir: p.workspaceDir ?? null });
|
|
11
|
+
break;
|
|
12
|
+
}
|
|
13
|
+
case 'chain/executing':
|
|
14
|
+
case 'chain/completed':
|
|
15
|
+
case 'chain/aborted': {
|
|
16
|
+
const c = state.chains.get(ev.chainId);
|
|
17
|
+
if (!c)
|
|
18
|
+
throw new Error('projection: unknown chain ' + ev.chainId);
|
|
19
|
+
next.chains = new Map(state.chains).set(ev.chainId, { ...c, status: transitionChain(c.status, ev.kind) });
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
case 'chain/root-task-set': {
|
|
23
|
+
const c = state.chains.get(ev.chainId);
|
|
24
|
+
if (!c)
|
|
25
|
+
throw new Error('projection: unknown chain ' + ev.chainId);
|
|
26
|
+
next.chains = new Map(state.chains).set(ev.chainId, { ...c, rootTaskId: String(ev.payload['rootTaskId'] ?? '') });
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
// D23:audit 事件不改 Chain 状态,只写验收核对视图(auditWarnings)
|
|
30
|
+
case 'chain/audit-warning': {
|
|
31
|
+
const c = state.chains.get(ev.chainId);
|
|
32
|
+
if (!c)
|
|
33
|
+
throw new Error('projection: unknown chain ' + ev.chainId);
|
|
34
|
+
const evidence = (ev.payload['evidence'] ?? []);
|
|
35
|
+
const audit = {
|
|
36
|
+
evidence,
|
|
37
|
+
warnedAt: ev.at,
|
|
38
|
+
warnedSeq: ev.seq,
|
|
39
|
+
confirmedAt: null,
|
|
40
|
+
confirmedBy: null,
|
|
41
|
+
confirmedSeq: null,
|
|
42
|
+
};
|
|
43
|
+
next.auditWarnings = new Map(state.auditWarnings).set(ev.chainId, audit);
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
case 'chain/audit-confirmed': {
|
|
47
|
+
const c = state.chains.get(ev.chainId);
|
|
48
|
+
if (!c)
|
|
49
|
+
throw new Error('projection: unknown chain ' + ev.chainId);
|
|
50
|
+
const existing = state.auditWarnings.get(ev.chainId);
|
|
51
|
+
if (!existing)
|
|
52
|
+
throw new Error('projection: audit-confirmed without audit-warning: ' + ev.chainId);
|
|
53
|
+
next.auditWarnings = new Map(state.auditWarnings).set(ev.chainId, {
|
|
54
|
+
...existing,
|
|
55
|
+
confirmedAt: ev.at,
|
|
56
|
+
confirmedBy: ev.author,
|
|
57
|
+
confirmedSeq: ev.seq,
|
|
58
|
+
});
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
// T7:链标题改名(非状态转换,只更新 title;快照重放可见)
|
|
62
|
+
case 'chain/title-updated': {
|
|
63
|
+
const c = state.chains.get(ev.chainId);
|
|
64
|
+
if (!c)
|
|
65
|
+
throw new Error('projection: unknown chain ' + ev.chainId);
|
|
66
|
+
next.chains = new Map(state.chains).set(ev.chainId, { ...c, title: String(ev.payload['to'] ?? '') });
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
// T7:任务标题改名(非状态转换,只更新 title)
|
|
70
|
+
case 'task/renamed': {
|
|
71
|
+
if (!ev.taskId)
|
|
72
|
+
throw new Error('projection: event without taskId');
|
|
73
|
+
const t = state.tasks.get(ev.taskId);
|
|
74
|
+
if (!t)
|
|
75
|
+
throw new Error('projection: unknown task ' + ev.taskId);
|
|
76
|
+
next.tasks = new Map(state.tasks).set(ev.taskId, { ...t, title: String(ev.payload['to'] ?? '') });
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
case 'task/created': {
|
|
80
|
+
const p = ev.payload;
|
|
81
|
+
if (!p.id || !p.assignee)
|
|
82
|
+
throw new Error('projection: malformed task/created');
|
|
83
|
+
// 归一化缺省字段,使最小 payload 的事件日志可回放(P0-3 回放为权威)
|
|
84
|
+
const normalized = {
|
|
85
|
+
...p,
|
|
86
|
+
status: p.status ?? 'todo',
|
|
87
|
+
parents: p.parents ?? [],
|
|
88
|
+
children: p.children ?? [],
|
|
89
|
+
attempts: p.attempts ?? 0,
|
|
90
|
+
heartbeats: p.heartbeats ?? [],
|
|
91
|
+
sessionId: p.sessionId ?? 'kbn-' + p.id,
|
|
92
|
+
reworkOfTaskId: p.reworkOfTaskId ?? null,
|
|
93
|
+
resumeSessionId: p.resumeSessionId ?? null,
|
|
94
|
+
reviewAttempt: p.reviewAttempt ?? 0,
|
|
95
|
+
reviewStatus: p.reviewStatus ?? 'not-required',
|
|
96
|
+
};
|
|
97
|
+
next.tasks = new Map(state.tasks).set(p.id, normalized);
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
case 'task/claimed':
|
|
101
|
+
case 'task/heartbeat':
|
|
102
|
+
case 'task/completed':
|
|
103
|
+
case 'task/blocked':
|
|
104
|
+
case 'task/unblocked':
|
|
105
|
+
case 'task/failed':
|
|
106
|
+
case 'task/archived': {
|
|
107
|
+
if (!ev.taskId)
|
|
108
|
+
throw new Error('projection: event without taskId');
|
|
109
|
+
const t = state.tasks.get(ev.taskId);
|
|
110
|
+
if (!t)
|
|
111
|
+
throw new Error('projection: unknown task ' + ev.taskId);
|
|
112
|
+
const updated = { ...t, status: transitionTask(t.status, ev.kind) };
|
|
113
|
+
if (ev.kind === 'task/heartbeat')
|
|
114
|
+
updated.heartbeats = [...t.heartbeats, ev.at];
|
|
115
|
+
// RC4:infra 失败不计入 attempts(瞬时基础设施错误不烧重试预算);任务质量失败正常 +1
|
|
116
|
+
if (ev.kind === 'task/failed' && !ev.payload['infra'])
|
|
117
|
+
updated.attempts = t.attempts + 1;
|
|
118
|
+
if (ev.kind === 'task/unblocked')
|
|
119
|
+
updated.attempts = 0; // RC4:人工解除阻塞 → 重试预算重置
|
|
120
|
+
if (ev.kind === 'task/completed') {
|
|
121
|
+
const p = ev.payload;
|
|
122
|
+
next.handoffs = new Map(state.handoffs).set(ev.taskId, p);
|
|
123
|
+
}
|
|
124
|
+
next.tasks = new Map(state.tasks).set(ev.taskId, updated);
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
// 评审事件(交付质量链):非状态转换——按 payload.targetTaskId 更新被评审任务 reviewStatus
|
|
128
|
+
case 'review/passed':
|
|
129
|
+
case 'review/failed':
|
|
130
|
+
case 'review/gave-up': {
|
|
131
|
+
const targetId = String(ev.payload['targetTaskId'] ?? '');
|
|
132
|
+
const t = state.tasks.get(targetId);
|
|
133
|
+
if (!t)
|
|
134
|
+
throw new Error('projection: unknown review target ' + targetId);
|
|
135
|
+
const status = ev.kind === 'review/passed' ? 'passed' : ev.kind === 'review/failed' ? 'failed' : 'gave-up';
|
|
136
|
+
next.tasks = new Map(state.tasks).set(targetId, { ...t, reviewStatus: status });
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
case 'spec-card/created':
|
|
140
|
+
case 'spec-card/edited': {
|
|
141
|
+
const p = ev.payload;
|
|
142
|
+
next.specCards = new Map(state.specCards).set(p.id, p);
|
|
143
|
+
// 规格卡创建时把链路的 specCardId 一并接线(事件溯源派生,无需额外事件)
|
|
144
|
+
if (ev.kind === 'spec-card/created') {
|
|
145
|
+
const c = state.chains.get(p.chainId);
|
|
146
|
+
if (c && !c.specCardId) {
|
|
147
|
+
next.chains = new Map(state.chains).set(p.chainId, { ...c, specCardId: p.id });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
case 'spec-card/approved': {
|
|
153
|
+
const id = String(ev.payload['id'] ?? '');
|
|
154
|
+
const s = state.specCards.get(id);
|
|
155
|
+
if (!s)
|
|
156
|
+
throw new Error('projection: unknown spec card ' + id);
|
|
157
|
+
next.specCards = new Map(state.specCards).set(id, { ...s, status: transitionSpecCard(s.status, ev.kind), approvedAt: ev.at, approvedBy: ev.author });
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return next;
|
|
162
|
+
}
|
|
163
|
+
export function project(events) {
|
|
164
|
+
return events.reduce(applyTo, empty());
|
|
165
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Handoff } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* 评审证据机械校验(交付质量链 PT/DT 共用):
|
|
4
|
+
* PT/DT 完成交接 metadata.review_evidence 必须满足角色所需的证据结构,
|
|
5
|
+
* 缺证据拒绝 pass(评审卡 complete 被拒 / orchestrator 不推进)。
|
|
6
|
+
*
|
|
7
|
+
* - PT(计划评审):必需 verdict + issues + 计划结构字段(对齐需求/完整性/逻辑交互一致性的产物引用)。
|
|
8
|
+
* - DT(实现评审):必需 verdict + test(exit 0) + build/typecheck/lint + diff 非空 + git 证据 +
|
|
9
|
+
* ocr/fallback 结论 + issues 处置。
|
|
10
|
+
*
|
|
11
|
+
* @returns 缺失清单(空数组 = 证据完整)。
|
|
12
|
+
*/
|
|
13
|
+
export declare function validateReviewEvidence(role: 'pt' | 'dt', handoff: Handoff | undefined): string[];
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 评审证据机械校验(交付质量链 PT/DT 共用):
|
|
3
|
+
* PT/DT 完成交接 metadata.review_evidence 必须满足角色所需的证据结构,
|
|
4
|
+
* 缺证据拒绝 pass(评审卡 complete 被拒 / orchestrator 不推进)。
|
|
5
|
+
*
|
|
6
|
+
* - PT(计划评审):必需 verdict + issues + 计划结构字段(对齐需求/完整性/逻辑交互一致性的产物引用)。
|
|
7
|
+
* - DT(实现评审):必需 verdict + test(exit 0) + build/typecheck/lint + diff 非空 + git 证据 +
|
|
8
|
+
* ocr/fallback 结论 + issues 处置。
|
|
9
|
+
*
|
|
10
|
+
* @returns 缺失清单(空数组 = 证据完整)。
|
|
11
|
+
*/
|
|
12
|
+
export function validateReviewEvidence(role, handoff) {
|
|
13
|
+
const missing = [];
|
|
14
|
+
if (!handoff)
|
|
15
|
+
return ['handoff missing'];
|
|
16
|
+
const ev = (handoff.metadata ?? {})['review_evidence'];
|
|
17
|
+
if (!ev || typeof ev !== 'object')
|
|
18
|
+
return ['review_evidence missing'];
|
|
19
|
+
if (ev.verdict !== 'pass' && ev.verdict !== 'fail')
|
|
20
|
+
missing.push('review_evidence.verdict');
|
|
21
|
+
if (!Array.isArray(ev.issues))
|
|
22
|
+
missing.push('review_evidence.issues');
|
|
23
|
+
if (role === 'pt') {
|
|
24
|
+
// 计划结构字段:评审须给出被评审计划产物引用(如 artifacts_path/page 结构)——缺则视为证据不足
|
|
25
|
+
const planRef = ev.reviewPage ?? (handoff.metadata ?? {})['artifacts_path'];
|
|
26
|
+
if (!planRef)
|
|
27
|
+
missing.push('review_evidence.plan (artifacts_path/reviewPage)');
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
// DT:实证校验证据(fail 评审同样必须已实证——test 字段必须存在;仅 pass 要求 exit 0)
|
|
31
|
+
if (!ev.test || typeof ev.test !== 'object')
|
|
32
|
+
missing.push('review_evidence.test');
|
|
33
|
+
else if (ev.verdict === 'pass' && ev.test['exit'] !== 0)
|
|
34
|
+
missing.push('review_evidence.test (exit 0)');
|
|
35
|
+
const buildOk = ev.build && typeof ev.build === 'object';
|
|
36
|
+
const tcOk = ev.typecheck && typeof ev.typecheck === 'object';
|
|
37
|
+
if (!buildOk && !tcOk)
|
|
38
|
+
missing.push('review_evidence.build/typecheck');
|
|
39
|
+
if (ev.lint === undefined)
|
|
40
|
+
missing.push('review_evidence.lint');
|
|
41
|
+
const diffOk = ev.diff && typeof ev.diff === 'object' && Object.keys(ev.diff).length > 0;
|
|
42
|
+
if (!diffOk)
|
|
43
|
+
missing.push('review_evidence.diff (non-empty)');
|
|
44
|
+
if (!ev.git || typeof ev.git !== 'object')
|
|
45
|
+
missing.push('review_evidence.git');
|
|
46
|
+
if (!ev.openCodeReview || typeof ev.openCodeReview !== 'object')
|
|
47
|
+
missing.push('review_evidence.openCodeReview (ocr/fallback)');
|
|
48
|
+
// TDD 硬要求(2026-08-26):代码变更必须带 tdd 证据(DT 只读 git 核验 test_first)。
|
|
49
|
+
// 镜像 test.exit 非对称:pass 必须 test_first===true;fail 允许 false 如实记录违规。
|
|
50
|
+
const tdd = ev.tdd;
|
|
51
|
+
if (!tdd || typeof tdd !== 'object') {
|
|
52
|
+
missing.push('review_evidence.tdd');
|
|
53
|
+
}
|
|
54
|
+
else if (tdd.skipped && typeof tdd.skipped === 'object') {
|
|
55
|
+
// skipped 与 test_files 二选一(XOR):并存视为非法声明
|
|
56
|
+
if (Array.isArray(tdd.test_files) && tdd.test_files.length > 0) {
|
|
57
|
+
missing.push('review_evidence.tdd (skipped XOR test_files)');
|
|
58
|
+
}
|
|
59
|
+
if (typeof tdd.skipped['reason'] !== 'string' || !tdd.skipped['reason'].trim()) {
|
|
60
|
+
missing.push('review_evidence.tdd (skipped.reason)');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
if (!Array.isArray(tdd.test_files) || tdd.test_files.length === 0) {
|
|
65
|
+
missing.push('review_evidence.tdd (test_files)');
|
|
66
|
+
}
|
|
67
|
+
if (ev.test && typeof ev.test === 'object' && ev.test['runner'] !== 'vitest') {
|
|
68
|
+
missing.push('review_evidence.test (runner=vitest)');
|
|
69
|
+
}
|
|
70
|
+
if (ev.verdict === 'pass' && tdd.test_first !== true) {
|
|
71
|
+
missing.push('review_evidence.tdd (test_first=true)');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return missing;
|
|
76
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { TaskStatus, ChainStatus, SpecCardStatus, EventKind } from './types.js';
|
|
2
|
+
export declare const transitionTask: (c: TaskStatus, k: EventKind) => TaskStatus;
|
|
3
|
+
export declare const transitionChain: (c: ChainStatus, k: EventKind) => ChainStatus;
|
|
4
|
+
export declare const transitionSpecCard: (c: SpecCardStatus, k: EventKind) => SpecCardStatus;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const TASK_TRANSITIONS = {
|
|
2
|
+
triage: { 'task/claimed': 'running' },
|
|
3
|
+
todo: { 'task/claimed': 'running', 'task/archived': 'archived' },
|
|
4
|
+
ready: { 'task/claimed': 'running', 'task/archived': 'archived' },
|
|
5
|
+
running: { 'task/completed': 'done', 'task/blocked': 'blocked', 'task/failed': 'failed', 'task/heartbeat': 'running' },
|
|
6
|
+
blocked: { 'task/unblocked': 'ready', 'task/archived': 'archived' },
|
|
7
|
+
done: { 'task/archived': 'archived' },
|
|
8
|
+
failed: { 'task/claimed': 'running', 'task/blocked': 'blocked', 'task/archived': 'archived' },
|
|
9
|
+
archived: {},
|
|
10
|
+
};
|
|
11
|
+
const CHAIN_TRANSITIONS = {
|
|
12
|
+
planning: { 'chain/executing': 'executing' },
|
|
13
|
+
executing: { 'chain/completed': 'completed', 'chain/aborted': 'aborted' },
|
|
14
|
+
completed: {},
|
|
15
|
+
aborted: {},
|
|
16
|
+
};
|
|
17
|
+
// D23:'chain/audit-warning' / 'chain/audit-confirmed' 不是状态转换——
|
|
18
|
+
// Chain 保持 completed,只向投影写入验收核对视图(auditWarnings)。
|
|
19
|
+
// projection.ts 对这两类事件专门处理、不调用 transitionChain,故此处不注册。
|
|
20
|
+
const SPEC_TRANSITIONS = {
|
|
21
|
+
draft: { 'spec-card/approved': 'approved' },
|
|
22
|
+
approved: {},
|
|
23
|
+
};
|
|
24
|
+
function step(name, table, current, kind) {
|
|
25
|
+
const next = table[current]?.[kind];
|
|
26
|
+
if (next === undefined)
|
|
27
|
+
throw new Error(`illegal transition: ${current} --${kind}--> (none)`);
|
|
28
|
+
return next;
|
|
29
|
+
}
|
|
30
|
+
export const transitionTask = (c, k) => step('task', TASK_TRANSITIONS, c, k);
|
|
31
|
+
export const transitionChain = (c, k) => step('chain', CHAIN_TRANSITIONS, c, k);
|
|
32
|
+
export const transitionSpecCard = (c, k) => step('spec', SPEC_TRANSITIONS, c, k);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Role, Task, TaskMode } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* 语义父依赖角色(不含状态判定):返回当前阶段任务"本该依赖"的上游 (assignee, mode)。
|
|
4
|
+
* 供 resolveTaskParents(取终态 id)与 createTask 硬拦(查非终态)共用,保证依赖推导单一来源。
|
|
5
|
+
* w:kb 特判:链上 d/execute 已交付 → 父=D(w3);否则父=P(w2)。
|
|
6
|
+
*/
|
|
7
|
+
export declare function semanticParentDeps(tasks: Iterable<Task>, chainId: string, assignee: Role, mode: TaskMode): Array<{
|
|
8
|
+
assignee: Role;
|
|
9
|
+
mode: TaskMode;
|
|
10
|
+
}>;
|
|
11
|
+
/** 语义上游中存在但非终态(非 done/archived)的父任务清单——createTask 硬拦依据。 */
|
|
12
|
+
export declare function nonTerminalSemanticParents(tasks: Iterable<Task>, chainId: string, assignee: Role, mode: TaskMode): Task[];
|
|
13
|
+
/**
|
|
14
|
+
* 解析某角色任务的语义父任务 id(终态 done/archived)。供 createTask 兜底与 V 建卡指令共用,
|
|
15
|
+
* 消除「V 建卡漏设 parents → 父交接注入断裂」的软约定风险(P 直接读规格卡,无 w1 预取父卡)。
|
|
16
|
+
*/
|
|
17
|
+
export declare function resolveTaskParents(tasks: Iterable<Task>, chainId: string, assignee: Role, mode: TaskMode): string[];
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 每阶段任务卡的语义输入来源(R20 依赖):第 1 项为主来源。
|
|
3
|
+
* key = `${assignee}:${mode}`;w:kb 特判(w2→P、w3→D)见 semanticParentDeps。
|
|
4
|
+
* 未列出的 mode(如旧兼容 align)不设语义 parents(返回空)。
|
|
5
|
+
*/
|
|
6
|
+
const PARENT_DEPS = {
|
|
7
|
+
'p:openspec': [], // v2:P 读规格卡(需求澄清清单附件)+ 父任务交接,无 w1 预取父卡
|
|
8
|
+
'pt:review-plan': [{ assignee: 'p', mode: 'openspec' }], // PT 评审 P 计划
|
|
9
|
+
'd:execute': [{ assignee: 'w', mode: 'kb' }], // D 接 W2(P 计划同步 KB 后的 page_path/kb_url,wiki_read 读实施计划执行)
|
|
10
|
+
'dt:review-impl': [{ assignee: 'd', mode: 'execute' }], // DT 评审 D 交付
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* 语义父依赖角色(不含状态判定):返回当前阶段任务"本该依赖"的上游 (assignee, mode)。
|
|
14
|
+
* 供 resolveTaskParents(取终态 id)与 createTask 硬拦(查非终态)共用,保证依赖推导单一来源。
|
|
15
|
+
* w:kb 特判:链上 d/execute 已交付 → 父=D(w3);否则父=P(w2)。
|
|
16
|
+
*/
|
|
17
|
+
export function semanticParentDeps(tasks, chainId, assignee, mode) {
|
|
18
|
+
if (assignee === 'w' && mode === 'kb') {
|
|
19
|
+
const dDone = [...tasks].some((t) => t.chainId === chainId && t.assignee === 'd' && t.mode === 'execute' &&
|
|
20
|
+
(t.status === 'done' || t.status === 'archived'));
|
|
21
|
+
return dDone ? [{ assignee: 'd', mode: 'execute' }] : [{ assignee: 'p', mode: 'openspec' }];
|
|
22
|
+
}
|
|
23
|
+
return PARENT_DEPS[`${assignee}:${mode}`] ?? [];
|
|
24
|
+
}
|
|
25
|
+
/** 语义上游中存在但非终态(非 done/archived)的父任务清单——createTask 硬拦依据。 */
|
|
26
|
+
export function nonTerminalSemanticParents(tasks, chainId, assignee, mode) {
|
|
27
|
+
const chainTasks = [...tasks].filter((t) => t.chainId === chainId);
|
|
28
|
+
const terminal = ['done', 'archived'];
|
|
29
|
+
return semanticParentDeps(chainTasks, chainId, assignee, mode).flatMap((d) => chainTasks.filter((t) => t.assignee === d.assignee && t.mode === d.mode && !terminal.includes(t.status)));
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* 解析某角色任务的语义父任务 id(终态 done/archived)。供 createTask 兜底与 V 建卡指令共用,
|
|
33
|
+
* 消除「V 建卡漏设 parents → 父交接注入断裂」的软约定风险(P 直接读规格卡,无 w1 预取父卡)。
|
|
34
|
+
*/
|
|
35
|
+
export function resolveTaskParents(tasks, chainId, assignee, mode) {
|
|
36
|
+
const chainTasks = [...tasks].filter((t) => t.chainId === chainId);
|
|
37
|
+
const ids = (a, m) => chainTasks.filter((t) => t.assignee === a && t.mode === m && (t.status === 'done' || t.status === 'archived')).map((t) => t.id);
|
|
38
|
+
return semanticParentDeps(chainTasks, chainId, assignee, mode).flatMap((d) => ids(d.assignee, d.mode));
|
|
39
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare function extOf(path: string): string;
|
|
2
|
+
/** 命中 *.test.* / *.spec.* / __tests__/ / tests/ 即测试文件。 */
|
|
3
|
+
export declare function isTestFile(path: string): boolean;
|
|
4
|
+
/** 需测试的源码:非测试文件且扩展名在约定集合内。 */
|
|
5
|
+
export declare function isTestableSource(path: string): boolean;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// src/domain/tdd-classify.ts
|
|
2
|
+
/** 需测试的源码扩展名(TDD 硬要求判定口径:含 js/jsx/vue)。 */
|
|
3
|
+
const TESTABLE_SOURCE_EXT = new Set(['ts', 'tsx', 'js', 'jsx', 'vue', 'rs', 'py', 'go', 'java']);
|
|
4
|
+
export function extOf(path) {
|
|
5
|
+
const m = /\.([A-Za-z0-9]+)$/.exec(path);
|
|
6
|
+
return m ? m[1].toLowerCase() : '';
|
|
7
|
+
}
|
|
8
|
+
/** 命中 *.test.* / *.spec.* / __tests__/ / tests/ 即测试文件。 */
|
|
9
|
+
export function isTestFile(path) {
|
|
10
|
+
const base = path.split('/').pop() ?? '';
|
|
11
|
+
if (/(\.test\.|\.spec\.)/i.test(base))
|
|
12
|
+
return true;
|
|
13
|
+
if (/__tests__\//.test(path) || /(^|\/)tests\//.test(path))
|
|
14
|
+
return true;
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
/** 需测试的源码:非测试文件且扩展名在约定集合内。 */
|
|
18
|
+
export function isTestableSource(path) {
|
|
19
|
+
return !isTestFile(path) && TESTABLE_SOURCE_EXT.has(extOf(path));
|
|
20
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
export type Role = 'v' | 'p' | 'w' | 'd' | 'pt' | 'dt';
|
|
2
|
+
export type TaskMode = 'file' | 'external' | 'kb' | 'openspec' | 'mattpocock' | 'align' | 'execute' | 'review-plan' | 'review-impl';
|
|
3
|
+
export type TaskStatus = 'triage' | 'todo' | 'ready' | 'running' | 'blocked' | 'done' | 'failed' | 'archived';
|
|
4
|
+
export type ChainStatus = 'planning' | 'executing' | 'completed' | 'aborted';
|
|
5
|
+
export type SpecCardStatus = 'draft' | 'approved';
|
|
6
|
+
/** 评审状态(交付质量链):not-required 普通卡 / pending 等待评审 / passed 通过 / failed 失败待返工 / gave-up 超限放弃。 */
|
|
7
|
+
export type ReviewStatus = 'not-required' | 'pending' | 'passed' | 'failed' | 'gave-up';
|
|
8
|
+
/** 评审结论:pass=通过 / fail=不通过。 */
|
|
9
|
+
export type ReviewVerdict = 'pass' | 'fail';
|
|
10
|
+
/** 评审问题条目(PT/DT 交接 evidence.issues)。 */
|
|
11
|
+
export interface ReviewIssue {
|
|
12
|
+
severity: 'critical' | 'high' | 'medium' | 'low';
|
|
13
|
+
title: string;
|
|
14
|
+
detail: string;
|
|
15
|
+
location?: string;
|
|
16
|
+
resolved: boolean;
|
|
17
|
+
}
|
|
18
|
+
/** TDD 硬要求证据(评审时收集的测试相关元信息)。 */
|
|
19
|
+
export interface TddEvidence {
|
|
20
|
+
test_files: string[];
|
|
21
|
+
test_first: boolean;
|
|
22
|
+
skipped?: {
|
|
23
|
+
reason: string;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** 评审证据(PT/DT 交接 metadata.review_evidence):机械校验通过才允许评审卡 pass。 */
|
|
27
|
+
export interface ReviewEvidence {
|
|
28
|
+
verdict: ReviewVerdict;
|
|
29
|
+
issues: ReviewIssue[];
|
|
30
|
+
tdd?: TddEvidence;
|
|
31
|
+
test?: Record<string, unknown>;
|
|
32
|
+
build?: Record<string, unknown>;
|
|
33
|
+
typecheck?: Record<string, unknown>;
|
|
34
|
+
lint?: Record<string, unknown>;
|
|
35
|
+
diff?: Record<string, unknown>;
|
|
36
|
+
git?: Record<string, unknown>;
|
|
37
|
+
openCodeReview?: Record<string, unknown>;
|
|
38
|
+
reviewPage?: {
|
|
39
|
+
pagePath: string;
|
|
40
|
+
kbUrl: string;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export type EventKind = 'chain/created' | 'chain/executing' | 'chain/completed' | 'chain/aborted' | 'chain/root-task-set' | 'chain/audit-warning' | 'chain/audit-confirmed' | 'chain/title-updated' | 'spec-card/created' | 'spec-card/edited' | 'spec-card/approved' | 'task/created' | 'task/claimed' | 'task/heartbeat' | 'task/commented' | 'task/completed' | 'task/blocked' | 'task/unblocked' | 'task/archived' | 'task/failed' | 'task/renamed' | 'review/passed' | 'review/failed' | 'review/gave-up';
|
|
44
|
+
export interface SpecCardSections {
|
|
45
|
+
problem: string;
|
|
46
|
+
solution: string;
|
|
47
|
+
user_stories: string[];
|
|
48
|
+
impl_decisions: string[];
|
|
49
|
+
testing: string;
|
|
50
|
+
out_of_scope: string;
|
|
51
|
+
}
|
|
52
|
+
export interface SpecCardAttachment {
|
|
53
|
+
name: string;
|
|
54
|
+
kind: 'file-prefetch' | 'external' | 'kb' | 'other';
|
|
55
|
+
ref: string;
|
|
56
|
+
}
|
|
57
|
+
export interface Chain {
|
|
58
|
+
id: string;
|
|
59
|
+
title: string;
|
|
60
|
+
status: ChainStatus;
|
|
61
|
+
rootTaskId: string | null;
|
|
62
|
+
specCardId: string | null;
|
|
63
|
+
ownerSessionId: string;
|
|
64
|
+
/** 发起 /plan: 的主 agent 会话工作目录(Q5:角色会话统一创建于此,便于管理 profile 会话)。null=未捕获(GUI 建链),回退 kanban 存储。 */
|
|
65
|
+
workspaceDir: string | null;
|
|
66
|
+
createdAt: number;
|
|
67
|
+
}
|
|
68
|
+
export interface SpecCard {
|
|
69
|
+
id: string;
|
|
70
|
+
chainId: string;
|
|
71
|
+
status: SpecCardStatus;
|
|
72
|
+
sections: SpecCardSections;
|
|
73
|
+
attachments: SpecCardAttachment[];
|
|
74
|
+
rawDialogueRef: string | null;
|
|
75
|
+
approvedAt: number | null;
|
|
76
|
+
approvedBy: string | null;
|
|
77
|
+
}
|
|
78
|
+
export interface Task {
|
|
79
|
+
id: string;
|
|
80
|
+
chainId: string;
|
|
81
|
+
title: string;
|
|
82
|
+
body: string;
|
|
83
|
+
assignee: Role;
|
|
84
|
+
status: TaskStatus;
|
|
85
|
+
mode: TaskMode;
|
|
86
|
+
priority: number;
|
|
87
|
+
parents: string[];
|
|
88
|
+
children: string[];
|
|
89
|
+
createdBy: 'v' | 'human' | 'auto';
|
|
90
|
+
attempts: number;
|
|
91
|
+
heartbeats: number[];
|
|
92
|
+
/** 确定性会话 id(kbn-<taskId>),与角色会话 id 一致,供追踪定位与 resume。 */
|
|
93
|
+
sessionId: string;
|
|
94
|
+
/** 返工来源任务 id(评审失败 createReworkTask 生成);null=非返工卡。 */
|
|
95
|
+
reworkOfTaskId: string | null;
|
|
96
|
+
/** resume 会话 id(返工卡沿用被返工任务的会话,避免重头);null=默认 kbn-<taskId>。 */
|
|
97
|
+
resumeSessionId: string | null;
|
|
98
|
+
/** 返工尝试次数(从被返工任务继承 +1)。 */
|
|
99
|
+
reviewAttempt: number;
|
|
100
|
+
/** 评审状态(普通卡 not-required)。 */
|
|
101
|
+
reviewStatus: ReviewStatus;
|
|
102
|
+
}
|
|
103
|
+
export interface Handoff {
|
|
104
|
+
summary: string;
|
|
105
|
+
metadata: Record<string, unknown>;
|
|
106
|
+
completedAt: number;
|
|
107
|
+
}
|
|
108
|
+
/** D23 链完成验收核对证据:主会话越权写工作区产物的单个线索。 */
|
|
109
|
+
export interface AuditEvidence {
|
|
110
|
+
source: 'main-session-scan' | 'artifact-reconciliation' | string;
|
|
111
|
+
detail: string;
|
|
112
|
+
paths: string[];
|
|
113
|
+
at?: number;
|
|
114
|
+
}
|
|
115
|
+
/** D23 链完成验收核对投影视图:warning 事件 + (可选)用户确认。 */
|
|
116
|
+
export interface ChainAudit {
|
|
117
|
+
evidence: AuditEvidence[];
|
|
118
|
+
warnedAt: number;
|
|
119
|
+
warnedSeq: number;
|
|
120
|
+
confirmedAt: number | null;
|
|
121
|
+
confirmedBy: string | null;
|
|
122
|
+
confirmedSeq: number | null;
|
|
123
|
+
}
|
|
124
|
+
/** 审计事件不是状态转换(不改变 Chain 状态,见 state-machine.ts)。 */
|
|
125
|
+
export declare const isAuditEventKind: (kind: EventKind) => boolean;
|
|
126
|
+
export interface KanbanEvent {
|
|
127
|
+
seq: number;
|
|
128
|
+
chainId: string;
|
|
129
|
+
taskId: string | null;
|
|
130
|
+
kind: EventKind;
|
|
131
|
+
payload: Record<string, unknown>;
|
|
132
|
+
author: string;
|
|
133
|
+
at: number;
|
|
134
|
+
}
|
|
135
|
+
export interface BoardState {
|
|
136
|
+
chains: Map<string, Chain>;
|
|
137
|
+
tasks: Map<string, Task>;
|
|
138
|
+
specCards: Map<string, SpecCard>;
|
|
139
|
+
handoffs: Map<string, Handoff>;
|
|
140
|
+
auditWarnings: Map<string, ChainAudit>;
|
|
141
|
+
events: KanbanEvent[];
|
|
142
|
+
}
|
package/lib/index.d.ts
ADDED