agentic-flow-kit 1.0.1 → 1.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/README.md +1 -1
- package/package.json +1 -1
- package/src/add-host.mjs +3 -2
- package/src/cli.mjs +1 -1
- package/src/init.mjs +2 -1
- package/src/profiles.mjs +6 -2
- package/src/sync.test.mjs +3 -1
- package/templates/AGENTS.md +1 -1
- package/templates/_agents/board/index.html +203 -5
- package/templates/_agents/scripts/check-loop.mjs +3 -3
- package/templates/_agents/scripts/check-loop.test.mjs +65 -3
- package/templates/_agents/scripts/pipeline-run.mjs +2 -2
- package/templates/_agents/scripts/workflow-board-server.mjs +92 -10
- package/templates/_agents/scripts/workflow-board-server.test.mjs +38 -1
- package/templates/workflow/plans/_TEMPLATE.md +1 -1
package/README.md
CHANGED
|
@@ -54,7 +54,7 @@ npx agentic-flow-kit add-gate dotnet-ca
|
|
|
54
54
|
| 选项 | 说明 |
|
|
55
55
|
|------|------|
|
|
56
56
|
| `--stack` | `dotnet` / `node` / `python` / `go` / `none`(默认 none)——**门禁配置三处**:commit-check 条件编译检查(暂存对应扩展名时 commit 前自动构建)+ **质量检测**(`checks`:lint / 类型检查 / vet 等秒级确定性检查,存在对应配置文件(tsconfig / eslint / ruff / go.mod 等)才启用,没有则自动跳过;测试不放提交门——关单在 test.md 阶段门)、settings.json 自检验命令权限、AGENTS.md「项目适配区」命令预填 |
|
|
57
|
-
| `--hosts` | `zcode` / `opencode` / `trae` / `omp` / `claude` / `cursor` / `codex` 逗号多选(默认 zcode
|
|
57
|
+
| `--hosts` | `zcode` / `opencode` / `trae` / `omp` / `claude` / `cursor` / `codex` 逗号多选(默认 zcode)。七个宿主一律入库(2026-10-05 起 zcode/omp 撤销 localOnly——换宿主不丢文件、CI 克隆面与本仓一致);zcode 的会话级液态草稿(`.zcode/drafts/`、`.zcode/plans/`)是本机态、不该入库——init 不再自动写这条规则,请自行加进 `.gitignore`(本仓实例见根 `.gitignore`)。Claude Code 加载 `.claude/commands` 与 `.claude/agents`;Cursor 加载 `.cursor/commands`;Codex 读仓库根 `AGENTS.md` 与 `.codex/skills/`,`.codex/commands` 是同形薄转发 |
|
|
58
58
|
| `--board-port` | workflow 看板端口(默认 8933) |
|
|
59
59
|
| `--dir` | 目标项目根(默认当前目录) |
|
|
60
60
|
| `--force` | 覆盖已存在的同名文件(默认保守跳过) |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentic-flow-kit",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "AI-Native 闭环工作流 + wiki 知识层脚手架——flow-kit init 一条命令为新项目装好 .agents 工作流引擎(阶段命令/角色契约/门禁钩子/kb 检索/看板/规则面预算)+ workflow 文档闭环 + wiki 知识库,装完自包含、零运行时依赖",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
package/src/add-host.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// flow-kit add-host <宿主>:init 后补装宿主适配层。
|
|
2
2
|
// 渲染 modules/hosts/<h> → HOSTS[h].dir → managed 台账按 rel 去重补记 → options.hosts 补记
|
|
3
|
-
// → localOnly 宿主(
|
|
3
|
+
// → localOnly 宿主(2026-10-05 起暂无命中,见 profiles.mjs#HOSTS.localOnly).gitignore 追加 → doctor
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { renderTree } from './render.mjs';
|
|
@@ -51,7 +51,8 @@ export function addHost(args, pkgRoot) {
|
|
|
51
51
|
if (!already) kit.options.hosts = [...(kit.options.hosts || []), host];
|
|
52
52
|
fs.writeFileSync(kitPath, `${JSON.stringify(kit, null, 2)}\n`);
|
|
53
53
|
|
|
54
|
-
// localOnly 宿主进 .gitignore
|
|
54
|
+
// localOnly 宿主进 .gitignore(缺才补)。2026-10-05 起七个宿主一律 localOnly:false,该分支暂无宿主命中;
|
|
55
|
+
// 保留是为了未来重开「本机专属宿主」档位时不必改结构——判定单源在 profiles.mjs#HOSTS.localOnly。
|
|
55
56
|
if (HOSTS[host].localOnly) {
|
|
56
57
|
const giPath = path.join(target, '.gitignore');
|
|
57
58
|
const line = `${HOSTS[host].dir}/`;
|
package/src/cli.mjs
CHANGED
|
@@ -46,7 +46,7 @@ init 选项(全部可选,均有默认值):
|
|
|
46
46
|
条件编译检查(builds)与质量检测(checks:lint/类型/vet 等秒级确定性检查,
|
|
47
47
|
有对应配置文件才启用)、settings.json 自检验命令权限(allow)、
|
|
48
48
|
AGENTS.md「项目适配区」命令预填
|
|
49
|
-
--hosts <宿主列表> 逗号分隔:zcode,opencode,trae,omp,claude,cursor,codex(默认 zcode
|
|
49
|
+
--hosts <宿主列表> 逗号分隔:zcode,opencode,trae,omp,claude,cursor,codex(默认 zcode;七个宿主一律入库,不自动进 .gitignore)
|
|
50
50
|
--board-port <端口> workflow 看板端口(默认 8933)
|
|
51
51
|
--dir <目录> 目标项目根(默认当前目录)
|
|
52
52
|
--force 覆盖已存在的同名文件(默认保守跳过)
|
package/src/init.mjs
CHANGED
|
@@ -242,7 +242,8 @@ export async function init(args, pkgRoot) {
|
|
|
242
242
|
ownedGenerated.push({ rel, sha256: sha256(Buffer.from(content, 'utf8')) });
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
-
// 4) .gitignore
|
|
245
|
+
// 4) .gitignore 追加(缺哪条补哪条)。2026-10-05 起七个宿主一律 localOnly:false,
|
|
246
|
+
// 宿主目录不再自动进 .gitignore——实际只补 .agents/cache/ 这条运行时缓存。
|
|
246
247
|
const giPath = path.join(target, '.gitignore');
|
|
247
248
|
const giNeed = ['.agents/cache/', ...(hosts.filter((h) => HOSTS[h].localOnly).map((h) => `${HOSTS[h].dir}/`))];
|
|
248
249
|
let giText = fs.existsSync(giPath) ? fs.readFileSync(giPath, 'utf8') : '';
|
package/src/profiles.mjs
CHANGED
|
@@ -6,10 +6,14 @@
|
|
|
6
6
|
// pkg:scripts.build 表示 package.json 里该脚本为非空字符串。没有对应配置的项目自动跳过,
|
|
7
7
|
// 不 fail-closed 误拦。none = 不配置(项目日后自填)。
|
|
8
8
|
export const HOSTS = {
|
|
9
|
-
zcode
|
|
9
|
+
// 2026-10-05 宿主本地化撤销:zcode / omp 原 localOnly:true(init 与 add-host 会把 .zcode/、.omp/
|
|
10
|
+
// 整目录写进装户 .gitignore)。改为 false 后七个宿主一律入库,换宿主不丢文件、CI 克隆面与本仓一致。
|
|
11
|
+
// 代价是 zcode 的会话级液态草稿(.zcode/drafts/、.zcode/plans/)改由装户自己加 .gitignore 规则;
|
|
12
|
+
// 本仓 .gitignore 净效果保留这两行草稿规则(实例见根 .gitignore),.zcode/agents/*.md 保持 tracked(2026-09-27 gate-coverage 独立复核确立)。
|
|
13
|
+
zcode: { dir: '.zcode', localOnly: false },
|
|
10
14
|
opencode: { dir: '.opencode', localOnly: false, commandPrefix: 'wf-' },
|
|
11
15
|
trae: { dir: '.trae', localOnly: false, commandPrefix: 'wf-' },
|
|
12
|
-
omp: { dir: '.omp', localOnly:
|
|
16
|
+
omp: { dir: '.omp', localOnly: false },
|
|
13
17
|
// 2026-09-29 店面:序号仍把 zcode 放第一(交互菜单 1–4 不变)。claude / cursor 的 commands 由宿主自动加载;
|
|
14
18
|
// codex 自动读仓库根 AGENTS.md 与 .codex/skills/,commands 与 agents 是同形薄转发,供显式引用。
|
|
15
19
|
claude: { dir: '.claude', localOnly: false, commandPrefix: 'wf-' },
|
package/src/sync.test.mjs
CHANGED
|
@@ -18,6 +18,8 @@ function check(name, cond, detail = '') {
|
|
|
18
18
|
}
|
|
19
19
|
const W = (p, content) => { fs.mkdirSync(path.dirname(p), { recursive: true }); fs.writeFileSync(p, content); };
|
|
20
20
|
const R = (p) => fs.readFileSync(p, 'utf8');
|
|
21
|
+
// 可选读:装户端在无 localOnly 宿主时不再创建 .gitignore,断言「不追加」须容忍文件不存在
|
|
22
|
+
const ROpt = (p) => (fs.existsSync(p) ? fs.readFileSync(p, 'utf8') : '');
|
|
21
23
|
|
|
22
24
|
// ---- fixture 包根(v2 形态:chg/host 变更、new 新增、gone 已删)----
|
|
23
25
|
function mkFixture() {
|
|
@@ -162,7 +164,7 @@ else if (cmd === 'add-gate') addGate(rest, root);
|
|
|
162
164
|
const kit = JSON.parse(R(path.join(t, '.agents/kit.json')));
|
|
163
165
|
check('S7 managed 台账补记(项目根相对路径)', kit.managed.some((f) => f.rel === '.zcode/h.txt'), JSON.stringify(kit.managed));
|
|
164
166
|
check('S7 options.hosts 补记', Array.isArray(kit.options.hosts) && kit.options.hosts.includes('zcode'));
|
|
165
|
-
check('S7 localOnly
|
|
167
|
+
check('S7 宿主目录不再进 .gitignore(localOnly 已撤销,zcode 与其余宿主同策)', !ROpt(path.join(t, '.gitignore')).includes('.zcode/'), ROpt(path.join(t, '.gitignore')));
|
|
166
168
|
const r2 = runCmd('add-host', fx, t, ['zcode']);
|
|
167
169
|
check('S7 重复装未 --force → 报错退出', r2.status === 1 && (r2.stderr || '').includes('--force'), r2.stderr);
|
|
168
170
|
}
|
package/templates/AGENTS.md
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
- `.githooks/`(经 `core.hooksPath` 挂载,本地 clone 后执行 `git config core.hooksPath .githooks`):pre-commit(闭环配对 / 泳道完整性 / wiki 台账 / 规则面预算 / 双源一致性 / managed 台账快检 / 敏感信息扫描与条件构建)、pre-push(闭环断档扫描)、commit-msg、post-commit、pre-merge-commit。
|
|
27
27
|
- **`git commit` / `git merge` / `git push` 三处一律禁 `--no-verify`**——被拦说明产出不合规,按提示修完原路重试。
|
|
28
28
|
- 项目专属门禁挂 `.agents/hooks/local-pre-commit`。装户五条硬规则(配对、验收、确认留痕、敏感信息、双源台账)与 `audit` 档见 `workflow/README.md`「硬规则」。
|
|
29
|
-
- 提交遵循 Conventional Commits 中文(feat / fix / docs / style / refactor / perf
|
|
29
|
+
- 提交遵循 Conventional Commits 中文(feat / fix / docs / style / refactor / perf)。L1+ 三段式闭环链:`docs(*)` approved 留痕 → 代码 → 关单 `docs(*)`,相邻提交即同一闭环(2026-10-04 修订)。
|
|
30
30
|
- **确认门(2026-09-27 起)**:intent/spec/plan 的 approved/done 与 incident 的 fixed/closed(两跳、无单跳)唯一入口 = `node .agents/scripts/confirm-doc.mjs <path>`,放弃态走显式 `--to superseded|cancelled`(合法前态见脚本提示;四终态同样内容绑定),两形态:① 用户终端亲手运行键入「可以」(TTY,AI 会话内被拒);② **对话委托代录**——用户在对话内明确确认后,AI 跑 `confirm-doc.mjs <path> --delegated "<用户原话>"` 逐件代录(默认一次一份,多份并录被拒),台账如实记 `source: chat-delegated` + 原话供对质,永不伪装 TTY 行(check-loop 15 指纹+台账对账拦截,与 source 无关);L0/L1 协作道可 `--batch` 多份一次代录(台账 `brief:true`,15 并录告警豁免;L2/L3 仍逐份)。
|
|
31
31
|
|
|
32
32
|
### 检索、看板与量化
|
|
@@ -54,6 +54,24 @@
|
|
|
54
54
|
#q { background: var(--card); border: 1px solid var(--border); color: var(--text); border-radius: 3px; padding: 6px 10px; font: 12px/1.4 var(--mono); width: 200px; transition: border-color .15s ease-out; }
|
|
55
55
|
#q::placeholder { color: var(--faint); }
|
|
56
56
|
#q:focus { outline: none; border-color: var(--border-strong); }
|
|
57
|
+
.viewbtn { background: var(--card); border: 1px solid var(--border); color: var(--muted); border-radius: 3px; padding: 6px 10px; font: 12px/1.4 var(--mono); cursor: pointer; transition: border-color .15s ease-out, color .15s ease-out, background .15s ease-out; }
|
|
58
|
+
.viewbtn:hover { border-color: var(--border-strong); color: var(--text); }
|
|
59
|
+
.viewbtn.on { background: var(--text); color: var(--bg); border-color: var(--text); }
|
|
60
|
+
|
|
61
|
+
/* 执行器视图:run 卡片列(2026-10-04 board-run-panel) */
|
|
62
|
+
#runs-view { display: flex; flex-direction: column; height: calc(100vh - 58px); overflow-y: auto; padding: 14px 22px 28px; gap: 10px; }
|
|
63
|
+
.run-card { background: var(--card); border: 1px solid var(--border); border-left: 3px solid var(--border-strong); border-radius: 4px; padding: 12px 14px; cursor: pointer; transition: background .15s ease-out, border-color .15s ease-out, transform .15s ease-out, box-shadow .15s ease-out; }
|
|
64
|
+
.run-card:hover { background: var(--card-hover); border-color: var(--border-strong); transform: translateY(-1px); box-shadow: var(--shadow-1); }
|
|
65
|
+
.run-card .row { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; margin-bottom: 7px; }
|
|
66
|
+
.run-card .title { font-size: 13px; line-height: 1.6; }
|
|
67
|
+
.run-bar { font: 12px/1.6 var(--mono); color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 4px; }
|
|
68
|
+
.run-bar b { color: var(--text); font-weight: 600; }
|
|
69
|
+
.stopchip { font: 500 10px/1 var(--mono); border-radius: 2px; padding: 2px 5px; }
|
|
70
|
+
.stopchip.work-order { color: var(--intent); border: 1px solid var(--intent); }
|
|
71
|
+
.stopchip.await-confirm { color: var(--warn); border: 1px solid var(--warn); }
|
|
72
|
+
.stopchip.gate-fail { color: var(--bad); border: 1px solid var(--bad); }
|
|
73
|
+
.stopchip.done { color: var(--ok); border: 1px solid var(--ok); }
|
|
74
|
+
.stopchip.aborted { color: var(--muted); border: 1px solid var(--border-strong); }
|
|
57
75
|
|
|
58
76
|
/* ---- 看板:去列盒子,发丝竖线分栏的编辑式构图 ---- */
|
|
59
77
|
#board { display: flex; overflow-x: auto; height: calc(100vh - 58px); }
|
|
@@ -162,9 +180,11 @@
|
|
|
162
180
|
<span class="chip" id="c-plans"></span><span class="chip" id="c-specs"></span>
|
|
163
181
|
<span class="chip" id="c-alerts" style="display:none"></span>
|
|
164
182
|
<input id="q" placeholder="过滤 标题 / slug / 文件…" spellcheck="false">
|
|
183
|
+
<button id="view-runs" class="viewbtn" title="pipeline-run 执行器事件流">执行器</button>
|
|
165
184
|
<span id="sse"><i></i><span>连接断开</span></span>
|
|
166
185
|
</header>
|
|
167
186
|
<div id="board"></div>
|
|
187
|
+
<div id="runs-view" style="display:none"></div>
|
|
168
188
|
|
|
169
189
|
<div id="drawer">
|
|
170
190
|
<div id="d-head">
|
|
@@ -176,6 +196,8 @@
|
|
|
176
196
|
<div id="d-tabs">
|
|
177
197
|
<button data-tab="doc" class="on">文档</button>
|
|
178
198
|
<button data-tab="hist">修改历史</button>
|
|
199
|
+
<button data-tab="timeline" class="run-tab" style="display:none">时间线</button>
|
|
200
|
+
<button data-tab="workorder" class="run-tab" style="display:none">工单</button>
|
|
179
201
|
</div>
|
|
180
202
|
<div id="d-body"></div>
|
|
181
203
|
</div>
|
|
@@ -198,6 +220,10 @@ let TAB = 'doc';
|
|
|
198
220
|
let retryTimer = null; // /api/board 失败后的重试定时器
|
|
199
221
|
let ROOT = ''; // 仓库根(/api/board 返回,内链 vscode 协议用)
|
|
200
222
|
let Q = ''; // 搜索过滤关键字
|
|
223
|
+
let RUNS = []; // 最近一次 /api/runs 的 run 卡片
|
|
224
|
+
let RUN_CURRENT = null; // 抽屉当前打开的 run
|
|
225
|
+
let VIEW = 'board'; // 当前视图:board | runs
|
|
226
|
+
let RUN_TAB = 'timeline'; // run 抽屉当前 tab:timeline | workorder
|
|
201
227
|
|
|
202
228
|
const $ = (s) => document.querySelector(s);
|
|
203
229
|
const esc = (s) => s.replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
|
|
@@ -343,7 +369,11 @@ function renderDrawer() {
|
|
|
343
369
|
const related = CARDS.filter((c) => c.slug === CURRENT.slug && c.file !== CURRENT.file);
|
|
344
370
|
$('#d-related').innerHTML = related.map((c) =>
|
|
345
371
|
`<a data-file="${esc(c.file)}">${TYPE_LABEL[c.type]} · ${esc(c.date)}${c.status !== CURRENT.status ? ' · ' + esc(c.status) : ''}</a>`).join('');
|
|
346
|
-
document.querySelectorAll('#d-tabs button').forEach((b) =>
|
|
372
|
+
document.querySelectorAll('#d-tabs button').forEach((b) => {
|
|
373
|
+
const isRunTab = b.classList.contains('run-tab');
|
|
374
|
+
b.style.display = isRunTab ? 'none' : '';
|
|
375
|
+
b.classList.toggle('on', b.dataset.tab === TAB);
|
|
376
|
+
});
|
|
347
377
|
if (TAB === 'doc') loadDoc(); else loadHist();
|
|
348
378
|
}
|
|
349
379
|
|
|
@@ -383,6 +413,159 @@ async function loadHist() {
|
|
|
383
413
|
: '<div class="none">暂无提交记录(文件尚未 commit;git 历史只包含已提交的修改)</div>';
|
|
384
414
|
}
|
|
385
415
|
|
|
416
|
+
// ================= 执行器视图(2026-10-04 board-run-panel) =================
|
|
417
|
+
// 只读 pipeline-runs/ 事件流(机器事实),不读 orchestration-runs.jsonl(自报态)。
|
|
418
|
+
const STOP_LABEL = {
|
|
419
|
+
'work-order': '工单', 'await-confirm': '等待确认', 'gate-fail': '门拒绝', done: '完成', aborted: '已放弃',
|
|
420
|
+
};
|
|
421
|
+
const EVENT_DESC = {
|
|
422
|
+
'run-created': '创建 run', 'work-order': '下发工单', triage: '定级', gate: '门执行',
|
|
423
|
+
confirm: '确认', commit: '提交', stop: '停机', done: '完成', abort: '放弃',
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
async function refreshRuns() {
|
|
427
|
+
let data;
|
|
428
|
+
try {
|
|
429
|
+
const resp = await fetch('/api/runs');
|
|
430
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
431
|
+
data = await resp.json();
|
|
432
|
+
} catch (e) {
|
|
433
|
+
return false;
|
|
434
|
+
}
|
|
435
|
+
RUNS = data.cards || []; // 键名 cards 对齐 /api/runs 载荷(intent 2026-10-04-board-run-panel 验收#1)
|
|
436
|
+
const q = Q.trim().toLowerCase();
|
|
437
|
+
const list = RUNS.filter((r) => !q || (r.runId + ' ' + r.requirement + ' ' + r.stage).toLowerCase().includes(q));
|
|
438
|
+
const html = list.length
|
|
439
|
+
? list.map((r) => {
|
|
440
|
+
const stop = STOP_LABEL[r.stopType] || esc(r.stopType || '');
|
|
441
|
+
const stopCls = r.stopType || '';
|
|
442
|
+
const bar = r.stageBar || '';
|
|
443
|
+
return `<div class="run-card" data-run="${esc(r.runId)}">` +
|
|
444
|
+
`<div class="row">` +
|
|
445
|
+
`<span class="type" style="background:var(--spec)">RUN</span>` +
|
|
446
|
+
`<span class="lv">${esc(r.triage?.level || '')}</span>` +
|
|
447
|
+
`<span class="stopchip ${esc(stopCls)}">${esc(stop)}</span>` +
|
|
448
|
+
`<span class="date">${esc(r.updatedAt?.slice(0, 16).replace('T', ' ') || '')}</span>` +
|
|
449
|
+
`</div>` +
|
|
450
|
+
`<div class="title">${esc(r.requirement || r.runId)}</div>` +
|
|
451
|
+
`<div class="run-bar">${esc(r.runId)} · <b>${esc(r.stage || '')}</b>${r.eventCount ? ` · ${r.eventCount} 事件` : ''}</div>` +
|
|
452
|
+
(bar ? `<div class="run-bar">${esc(bar)}</div>` : '') +
|
|
453
|
+
`</div>`;
|
|
454
|
+
}).join('')
|
|
455
|
+
: '<div class="empty">暂无 run</div>';
|
|
456
|
+
$('#runs-view').innerHTML = `<h2 style="font:600 11px/1 var(--mono);letter-spacing:.12em;color:var(--muted);padding:4px 0 10px;">执行器 · ${RUNS.length} runs</h2>` + html;
|
|
457
|
+
return true;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// —— run 抽屉:时间线 + 工单/确认详情 ——
|
|
461
|
+
function runEventHtml(run) {
|
|
462
|
+
const evs = run.events || [];
|
|
463
|
+
if (!evs.length) return '<div class="none">暂无事件</div>';
|
|
464
|
+
return `<div class="tl">${evs.map((e) => {
|
|
465
|
+
const t = EVENT_DESC[e.type] || e.type;
|
|
466
|
+
const sub = e.type === 'gate'
|
|
467
|
+
? `${esc(e.cmd || '')} · exit ${e.exit}${e.ms != null ? ` · ${e.ms}ms` : ''}`
|
|
468
|
+
: e.type === 'commit'
|
|
469
|
+
? `${esc(e.subject || '')} · ${esc(e.sha || '')}`
|
|
470
|
+
: e.type === 'work-order'
|
|
471
|
+
? `${esc(e.kind || '')} 工单 #${e.id}`
|
|
472
|
+
: e.type === 'stop'
|
|
473
|
+
? `${esc(e.stopType || '')}${e.stage ? ' · ' + esc(e.stage) : ''}${e.workOrderId != null ? ' · 工单 #' + e.workOrderId : ''}`
|
|
474
|
+
: e.type === 'confirm'
|
|
475
|
+
? `${esc(e.doc || '')} → ${esc(e.to || '')}${e.source ? ' · ' + esc(e.source) : ''}`
|
|
476
|
+
: e.type === 'abort'
|
|
477
|
+
? `→ ${esc(e.to || '')}`
|
|
478
|
+
: e.type === 'triage'
|
|
479
|
+
? `${esc(e.kind || '')} · ${esc(e.level || '')} · ${esc(e.module || '')}`
|
|
480
|
+
: '';
|
|
481
|
+
return `<div class="tl-item"><div class="tl-sub">${esc(t)}</div>` +
|
|
482
|
+
(sub ? `<div class="tl-meta">${sub}</div>` : '') +
|
|
483
|
+
`<div class="tl-meta">${esc((e.t || '').slice(0, 19).replace('T', ' '))}</div></div>`;
|
|
484
|
+
}).join('')}</div>`;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function loadRunTimeline() {
|
|
488
|
+
if (!RUN_CURRENT) return;
|
|
489
|
+
$('#d-body').innerHTML = runEventHtml(RUN_CURRENT);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function loadRunWorkOrder() {
|
|
493
|
+
if (!RUN_CURRENT) return;
|
|
494
|
+
const w = RUN_CURRENT.workOrder;
|
|
495
|
+
if (w) {
|
|
496
|
+
$('#d-body').innerHTML =
|
|
497
|
+
`<div class="md"><h2>工单 #${w.id} · ${esc(w.kind)}</h2>` +
|
|
498
|
+
`<p><b>${esc(w.title || '')}</b></p>` +
|
|
499
|
+
`<p>目标:${esc(w.goal || '')}</p>` +
|
|
500
|
+
(w.instructions?.length ? `<ul>${w.instructions.map((s) => `<li>${esc(s)}</li>`).join('')}</ul>` : '') +
|
|
501
|
+
(w.context?.kbHits?.length ? `<p>查重命中:</p><ul>${w.context.kbHits.slice(0, 6).map((s) => `<li>${esc(s)}</li>`).join('')}</ul>` : '') +
|
|
502
|
+
(w.context?.stderrTail ? `<pre><code>${esc(w.context.stderrTail)}</code></pre>` : '') +
|
|
503
|
+
(w.constraints?.length ? `<p>约束:</p><ul>${w.constraints.map((s) => `<li>${esc(s)}</li>`).join('')}</ul>` : '') +
|
|
504
|
+
(w.acceptance?.length ? `<p>验收:</p><ul>${w.acceptance.map((s) => `<li>${esc(s)}</li>`).join('')}</ul>` : '') +
|
|
505
|
+
`</div>`;
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
// 停机在确认门:渲染确认门要点(intent 2026-10-04-board-run-panel 目标节「工单详情 + 确认门要点」)
|
|
509
|
+
const c = RUN_CURRENT.awaitConfirm;
|
|
510
|
+
if (c) {
|
|
511
|
+
$('#d-body').innerHTML =
|
|
512
|
+
`<div class="md"><h2>等待确认</h2>` +
|
|
513
|
+
`<p>文档:<code>${esc(c.doc || '')}</code></p>` +
|
|
514
|
+
`<p>确认门要点:${esc(c.points || '')}</p>` +
|
|
515
|
+
`<p>${c.ledger ? '将经 confirm-doc 代录入台账(对话内明确确认后逐份代录,台账记原话)。' : '对话级确认,记 run 事件不进台账。'}</p>` +
|
|
516
|
+
`</div>`;
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
$('#d-body').innerHTML = '<div class="none">当前无工单(已停机在非工单点或已完成)</div>';
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function renderRunDrawer() {
|
|
523
|
+
if (!RUN_CURRENT) return;
|
|
524
|
+
$('#d-badges').innerHTML =
|
|
525
|
+
`<span class="type" style="background:var(--spec)">RUN</span>` +
|
|
526
|
+
(RUN_CURRENT.triage?.level ? `<span class="lv">${esc(RUN_CURRENT.triage.level)}</span>` : '') +
|
|
527
|
+
` <span class="chip">${esc(RUN_CURRENT.stopType || '')}</span>` +
|
|
528
|
+
` <span class="chip">${esc(RUN_CURRENT.stage || '')}</span>`;
|
|
529
|
+
$('#d-title').textContent = RUN_CURRENT.requirement || RUN_CURRENT.runId;
|
|
530
|
+
$('#d-related').innerHTML = `<a class="runid">${esc(RUN_CURRENT.runId)}</a>`;
|
|
531
|
+
// run 抽屉:显示时间线/工单 tab,隐藏文档/历史 tab
|
|
532
|
+
document.querySelectorAll('#d-tabs button').forEach((b) => {
|
|
533
|
+
const isRunTab = b.classList.contains('run-tab');
|
|
534
|
+
b.style.display = isRunTab ? '' : 'none';
|
|
535
|
+
b.classList.toggle('on', b.dataset.tab === RUN_TAB);
|
|
536
|
+
});
|
|
537
|
+
if (RUN_TAB === 'timeline') loadRunTimeline(); else loadRunWorkOrder();
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
async function openRun(runId) {
|
|
541
|
+
RUN_CURRENT = null;
|
|
542
|
+
$('#drawer').classList.add('open');
|
|
543
|
+
$('#d-title').textContent = '加载中…';
|
|
544
|
+
$('#d-body').innerHTML = '<div class="none">加载中…</div>';
|
|
545
|
+
try {
|
|
546
|
+
const resp = await fetch(`/api/run?file=${encodeURIComponent(runId + '.json')}`);
|
|
547
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
548
|
+
const data = await resp.json();
|
|
549
|
+
RUN_CURRENT = data.run || null;
|
|
550
|
+
if (!RUN_CURRENT) throw new Error('run 为空');
|
|
551
|
+
RUN_TAB = 'timeline';
|
|
552
|
+
renderRunDrawer();
|
|
553
|
+
} catch (e) {
|
|
554
|
+
$('#d-title').textContent = '加载失败';
|
|
555
|
+
$('#d-body').innerHTML = `<div class="none">无法加载 run:${esc(e.message)}</div>`;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function switchView(v) {
|
|
560
|
+
VIEW = v;
|
|
561
|
+
const isRuns = v === 'runs';
|
|
562
|
+
$('#board').style.display = isRuns ? 'none' : '';
|
|
563
|
+
$('#runs-view').style.display = isRuns ? '' : 'none';
|
|
564
|
+
$('#view-runs').classList.toggle('on', isRuns);
|
|
565
|
+
$('#q').placeholder = isRuns ? '过滤 runId / 需求 / 阶段…' : '过滤 标题 / slug / 文件…';
|
|
566
|
+
if (isRuns) refreshRuns(); else refreshBoard();
|
|
567
|
+
}
|
|
568
|
+
|
|
386
569
|
function openCard(card) { CURRENT = card; TAB = 'doc'; $('#drawer').classList.add('open'); renderDrawer(); }
|
|
387
570
|
|
|
388
571
|
// —— toast 反馈 ——
|
|
@@ -445,20 +628,35 @@ document.addEventListener('click', (e) => {
|
|
|
445
628
|
if (hash) navigator.clipboard.writeText(hash.dataset.h)
|
|
446
629
|
.then(() => toast('已复制 ' + hash.dataset.h.slice(0, 9)))
|
|
447
630
|
.catch(() => toast('复制失败'));
|
|
631
|
+
// 执行器 run 卡片 → 开抽屉(时间线)
|
|
632
|
+
const runCard = e.target.closest('.run-card');
|
|
633
|
+
if (runCard) {
|
|
634
|
+
const hit = RUNS.find((r) => r.runId === runCard.dataset.run);
|
|
635
|
+
if (hit) openRun(hit.runId);
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
448
638
|
});
|
|
449
639
|
$('#d-close').onclick = () => { $('#drawer').classList.remove('open'); };
|
|
450
640
|
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') $('#drawer').classList.remove('open'); });
|
|
451
|
-
|
|
641
|
+
// 抽屉 tab:run 抽屉(时间线/工单)与文档抽屉(文档/历史)分开处理
|
|
642
|
+
document.querySelectorAll('#d-tabs button').forEach((b) => b.onclick = () => {
|
|
643
|
+
if (b.classList.contains('run-tab') && RUN_CURRENT) { RUN_TAB = b.dataset.tab; renderRunDrawer(); }
|
|
644
|
+
else if (!b.classList.contains('run-tab')) { TAB = b.dataset.tab; renderDrawer(); }
|
|
645
|
+
});
|
|
646
|
+
$('#view-runs').onclick = () => switchView(VIEW === 'runs' ? 'board' : 'runs');
|
|
452
647
|
|
|
453
648
|
// SSE:服务端 fs.watch 防抖后推送 changed → 看板与打开中的抽屉自动刷新
|
|
454
649
|
const sse = new EventSource('/api/events');
|
|
455
|
-
sse.addEventListener('changed', () => {
|
|
650
|
+
sse.addEventListener('changed', () => {
|
|
651
|
+
refreshBoard().then((ok) => { if (ok) renderDrawer(); });
|
|
652
|
+
if (VIEW === 'runs') refreshRuns();
|
|
653
|
+
});
|
|
456
654
|
sse.addEventListener('watchdead', () => { $('#sse').classList.remove('on'); $('#sse').classList.add('poll'); $('#sse span:last-child').textContent = '监听失效 · 60s 轮询'; });
|
|
457
655
|
sse.onopen = () => { $('#sse').classList.add('on'); $('#sse').classList.remove('poll'); $('#sse span:last-child').textContent = '实时'; };
|
|
458
656
|
sse.onerror = () => { $('#sse').classList.remove('on'); $('#sse span:last-child').textContent = '连接断开,自动重连中'; };
|
|
459
657
|
|
|
460
658
|
// 60s 兜底轮询:SSE/watch 任何环节失效时数据仍保鲜(正常时多拉一次无害)
|
|
461
|
-
setInterval(() => refreshBoard(), 60000);
|
|
659
|
+
setInterval(() => { refreshBoard(); if (VIEW === 'runs') refreshRuns(); }, 60000);
|
|
462
660
|
|
|
463
661
|
// 搜索过滤:本地过滤标题/slug/文件,SSE 刷新时保持关键字
|
|
464
662
|
// 击键去抖 400ms(复核 P2-2 rider):/api/board 服务端有 loop TTL 缓存,去抖进一步避免连发请求排队
|
|
@@ -466,7 +664,7 @@ let qTimer = null;
|
|
|
466
664
|
$('#q').addEventListener('input', (e) => {
|
|
467
665
|
Q = e.target.value;
|
|
468
666
|
clearTimeout(qTimer);
|
|
469
|
-
qTimer = setTimeout(refreshBoard, 400);
|
|
667
|
+
qTimer = setTimeout(() => { if (VIEW === 'runs') refreshRuns(); else refreshBoard(); }, 400);
|
|
470
668
|
});
|
|
471
669
|
|
|
472
670
|
// 首次入场:列错峰上浮一次(.boot 稍后摘除,SSE / 过滤触发的重渲染不再重播)
|
|
@@ -504,7 +504,7 @@ for (const inc of docFiles('incidents')) {
|
|
|
504
504
|
}
|
|
505
505
|
}
|
|
506
506
|
const zc = `.zcode/agents/${role}.md`;
|
|
507
|
-
if (fs.existsSync(path.join(ROOT, zc))) { //
|
|
507
|
+
if (fs.existsSync(path.join(ROOT, zc))) { // .zcode/agents/ 现为入库件(2026-10-05 起宿主一律入库);缺失只说明宿主未装,不告警,存在时校验一致性
|
|
508
508
|
if (!new RegExp(`^name:\\s*["']?${role}["']?\\s*$`, 'm').test(textOf(zc).join('\n'))) warnings.push(`- [WARN Adapter 无效] ZCode ${role} 的 name 与文件名不一致:${zc}`);
|
|
509
509
|
if (!textOf(zc).some((l) => l.includes(rolePath))) warnings.push(`- [WARN Adapter 断线] ZCode ${role} 未引用公共角色:${rolePath}`);
|
|
510
510
|
}
|
|
@@ -596,10 +596,10 @@ function verifyEvidenceTruth(evidenceStr, planBase, root) {
|
|
|
596
596
|
|
|
597
597
|
const planLines = linesOf(planPath) || [];
|
|
598
598
|
const declaredFiles = [];
|
|
599
|
-
//
|
|
599
|
+
// 扫描「改动面」「任务拆解」或「改动方案」(L1 Quick-Plan 生成器 fill-plan 输出)节
|
|
600
600
|
let inDeclaredSec = false;
|
|
601
601
|
for (const line of planLines) {
|
|
602
|
-
if (/^##\s+(
|
|
602
|
+
if (/^##\s+(改动面|任务拆解|改动方案)/.test(line)) { inDeclaredSec = true; continue; }
|
|
603
603
|
if (inDeclaredSec && /^##\s+/.test(line)) { inDeclaredSec = false; continue; }
|
|
604
604
|
if (inDeclaredSec) {
|
|
605
605
|
const fileMatch = line.match(/([a-zA-Z0-9._\/-]+\.[a-zA-Z0-9]+)/);
|
|
@@ -16,6 +16,9 @@ import { entryConfirmed, MARK_RE, laneOfEntry, laneOfDoc } from './stage-gates.m
|
|
|
16
16
|
|
|
17
17
|
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
18
18
|
const CHECK_LOOP = path.join(SCRIPT_DIR, 'check-loop.mjs');
|
|
19
|
+
const GIT = process.platform === 'win32' ? 'git.exe' : 'git'; // Windows spawn 不补 .exe 扩展名(gitInit/gitCommitAll/场景 43/44/47 共用)
|
|
20
|
+
// 注意:check-loop.mjs 无 isMain 主守卫,被 import 即全量执行门禁并 process.exit——本套件只能端到端 spawn,
|
|
21
|
+
// 不可从其 import 函数直测(2026-10-04 review-fix-batch 实证;可 import 化记 papercuts 待 L2 重构)
|
|
19
22
|
|
|
20
23
|
let pass = 0;
|
|
21
24
|
let fail = 0;
|
|
@@ -250,7 +253,7 @@ const mkConfirmedDoc = (root, rel, fmBody) => {
|
|
|
250
253
|
rmfix(T);
|
|
251
254
|
}
|
|
252
255
|
|
|
253
|
-
// ---- 场景 15:ZCode Adapter 缺失不告警(
|
|
256
|
+
// ---- 场景 15:ZCode Adapter 缺失不告警(宿主未装),存在且断线仍告警 ----
|
|
254
257
|
{
|
|
255
258
|
const T = mkfix();
|
|
256
259
|
w(T, 'workflow/intents/2026-09-12-o.md', INTENT('o', '状态: done\n级别: L1\n日期: 2026-09-12', '\n## 验收标准(可测试)\n- [x] 用例通过(证据:全绿)\n'));
|
|
@@ -564,6 +567,65 @@ const mkConfirmedDoc = (root, rel, fmBody) => {
|
|
|
564
567
|
r.status === 1 && outOf(r).includes('验收缺证据'), `exit=${r.status}\n${outOf(r)}`);
|
|
565
568
|
rmfix(T);
|
|
566
569
|
}
|
|
570
|
+
// evidenceFixture:场景 43/44/47 共用脚手架——git 仓 + L1 done 三件(plan「改动方案」节声明 declared)+
|
|
571
|
+
// intent 证据引用唯一内容提交,返回 { T, sha }。createDeclared=false 时声明【从未存在】的文件(唯一内容
|
|
572
|
+
// 提交必然不触及,判定确定——2026-10-04 实测原「二次提交区分触及/不触及」方案在 npm test 环境偶发失败
|
|
573
|
+
// (git 时序敏感),负例改 ghost 构造)
|
|
574
|
+
const evidenceFixture = (slug, declared, createDeclared) => {
|
|
575
|
+
const T = mkfix();
|
|
576
|
+
gitInit(T);
|
|
577
|
+
if (createDeclared) {
|
|
578
|
+
fs.mkdirSync(path.join(T, path.dirname(declared)), { recursive: true });
|
|
579
|
+
w(T, declared, '// fixture\n');
|
|
580
|
+
}
|
|
581
|
+
w(T, `workflow/plans/2026-10-04-${slug}.md`, PLAN(slug, '状态: done\n级别: L1', `\n## 改动方案\n- ${declared}:声明文件${createDeclared ? '' : '(实际不存在,证据必不触及)'}\n`));
|
|
582
|
+
w(T, `workflow/intents/2026-10-04-${slug}.md`, INTENT(slug, '状态: done\n级别: L1\n日期: 2026-10-04',
|
|
583
|
+
'\n## 验收标准(可测试)\n- [x] 用例通过(证据:commit PLACEHOLDER)\n'));
|
|
584
|
+
gitCommitAll(T, `feat: ${slug} fixture`);
|
|
585
|
+
const sha = spawnSync(GIT, ['rev-parse', 'HEAD'], { cwd: T, encoding: 'utf8' }).stdout.trim().slice(0, 7);
|
|
586
|
+
w(T, `workflow/intents/2026-10-04-${slug}.md`, INTENT(slug, '状态: done\n级别: L1\n日期: 2026-10-04',
|
|
587
|
+
`\n## 验收标准(可测试)\n- [x] 用例通过(证据:commit ${sha})\n`));
|
|
588
|
+
gitCommitAll(T, `docs: ${slug} evidence`);
|
|
589
|
+
return { T, sha };
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
// ---- 场景 43:检查 8 证据真相——L1 plan「改动方案」节(fill-plan 输出)+ 证据 SHA 触及声明文件 → 不报「证据无关」(2026-10-04 plan-section-name-evidence 回归)----
|
|
593
|
+
{
|
|
594
|
+
const { T } = evidenceFixture('ev3', 'src/foo.js', true);
|
|
595
|
+
const r = run(T);
|
|
596
|
+
// fixture 不构造确认记录 → done 必触「确认未对账」hard(exit 1 预期);核心断言是证据面不出现「证据无关」
|
|
597
|
+
check('L1 plan「改动方案」节 + 证据触及声明文件 → 不报证据无关',
|
|
598
|
+
!outOf(r).includes('证据无关'), `exit=${r.status}\n${outOf(r)}`);
|
|
599
|
+
rmfix(T);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// ---- 场景 44:检查 8 证据真相——L1 plan「改动方案」节 + 证据 SHA 不触及声明文件 → 仍报「证据无关」(负例,防过度豁免)----
|
|
603
|
+
{
|
|
604
|
+
const { T } = evidenceFixture('ev4', 'src/ghost.js', false);
|
|
605
|
+
const r = run(T);
|
|
606
|
+
check('L1 plan「改动方案」节 + 证据不触及声明文件 → 仍报证据无关(负例)',
|
|
607
|
+
outOf(r).includes('证据无关'), `exit=${r.status}\n${outOf(r)}`);
|
|
608
|
+
rmfix(T);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// ---- 场景 47:检查 8 证据真相——证据 SHA 触及声明文件 → 校验通过 exit 0(2026-10-04 review-fix-batch:
|
|
612
|
+
// 补 spec 2026-10-04-plan-section-name-evidence §49「校验通过(type=sha)」断言强度——场景 43 的 fixture
|
|
613
|
+
// 触「确认未对账」hard 只能间接断言无「证据无关」;本场景沿场景 5x 造法(锚前自报日期豁免存量面)走满
|
|
614
|
+
// type=sha 放行路径:证据为真实 SHA + 声明文件被提交触及 + 提交无执行器标记 → exit 0 只能经 type=sha 达成
|
|
615
|
+
// (text 需无 hex 串 / process 需 runId 标记 / forged·irrelevant 均 exit 1,三条岔路均被构造排除)----
|
|
616
|
+
{
|
|
617
|
+
const T = mkfix();
|
|
618
|
+
gitInit(T);
|
|
619
|
+
w(T, 'workflow/plans/2026-09-26-sh1.md', PLAN('sh1', '状态: done\n级别: L1\n日期: 2026-09-26\n模块: material', '\n## 改动方案\n\n- src/app.mjs:实现功能\n'));
|
|
620
|
+
fs.mkdirSync(path.join(T, 'src'), { recursive: true });
|
|
621
|
+
w(T, 'src/app.mjs', '// fixture\n');
|
|
622
|
+
gitCommitAll(T, 'feat: sh1 实现(无执行器标记)');
|
|
623
|
+
const sha = spawnSync(GIT, ['rev-parse', '--short', 'HEAD'], { cwd: T, encoding: 'utf8' }).stdout.trim();
|
|
624
|
+
w(T, 'workflow/intents/2026-09-26-sh1.md', INTENT('sh1', '状态: done\n级别: L1\n日期: 2026-09-26', `\n## 验收标准(可测试)\n- [x] 用例通过(证据:${sha})\n`));
|
|
625
|
+
expectOk('证据 SHA 触及声明文件(无执行器标记)→ type=sha 校验通过 exit 0', T);
|
|
626
|
+
rmfix(T);
|
|
627
|
+
}
|
|
628
|
+
|
|
567
629
|
// ---- 场景 42:检查 4——fill-{intent,spec,plan}.mjs 花括号展开,三路皆存在 → 不报引用断档 ----
|
|
568
630
|
{
|
|
569
631
|
const T = mkfix();
|
|
@@ -577,7 +639,7 @@ const mkConfirmedDoc = (root, rel, fmBody) => {
|
|
|
577
639
|
r.status === 0 && !outOf(r).includes('引用断档'), `exit=${r.status}\n${outOf(r)}`);
|
|
578
640
|
rmfix(T);
|
|
579
641
|
}
|
|
580
|
-
// ---- 场景 43
|
|
642
|
+
// ---- 场景 45(原 43,2026-10-04 review-fix-batch 改号避让证据真相场景重号;旧号无文档按号引用):检查 4——花括号展开三路皆不存在 → 仍报引用断档(带完整展开式文案)----
|
|
581
643
|
{
|
|
582
644
|
const T = mkfix();
|
|
583
645
|
w(T, 'AGENTS.md', '# AGENTS\n\n起草先跑 `node .agents/scripts/fill-{alpha,beta,gamma}.mjs` 拿结构化草稿。\n');
|
|
@@ -588,7 +650,7 @@ const mkConfirmedDoc = (root, rel, fmBody) => {
|
|
|
588
650
|
r.status === 0 && /引用断档.*fill-\{alpha,beta,gamma\}\.mjs/.test(outOf(r)), `exit=${r.status}\n${outOf(r)}`);
|
|
589
651
|
rmfix(T);
|
|
590
652
|
}
|
|
591
|
-
// ---- 场景 44
|
|
653
|
+
// ---- 场景 46(原 44,同场景 45 缘由改号):检查 2——<主题> 与 .md 同行 = 命名约定描述 → 不报模板未填 ----
|
|
592
654
|
{
|
|
593
655
|
const T = mkfix();
|
|
594
656
|
w(T, 'workflow/intents/2026-09-01-nm1.md', INTENT('nm1', '状态: done\n级别: L1\n日期: 2026-09-01',
|
|
@@ -231,7 +231,7 @@ function stopDone(ctx, run) {
|
|
|
231
231
|
}
|
|
232
232
|
|
|
233
233
|
// ── 输出(人类可读 + 机器标记) ────────────────────────────────────────
|
|
234
|
-
function stageBar(run) {
|
|
234
|
+
export function stageBar(run) {
|
|
235
235
|
const s = run.stage;
|
|
236
236
|
const mk = (name, pos) => (pos === 'cur' ? '[●' + name + ']' : pos === 'past' ? '[✓' + name + ']' : '[…' + name + ']');
|
|
237
237
|
const seq = run.triage?.kind === 'verify' ? [['Verify', s === 'verify' || s === 'done']]
|
|
@@ -996,4 +996,4 @@ function adoptRun(ctx, a) {
|
|
|
996
996
|
console.log(' 提示:adopt 不推断断点位置——按文档现状人工续走(next 会按 stage 拒绝,请对照事件缺失情况手动接续或另起 run)');
|
|
997
997
|
}
|
|
998
998
|
|
|
999
|
-
export default { parseKV, validateDraftContent, closeoutComplete, placeholdersIn };
|
|
999
|
+
export default { parseKV, validateDraftContent, closeoutComplete, placeholdersIn, stageBar };
|
|
@@ -3,18 +3,23 @@
|
|
|
3
3
|
// 用法:node .agents/scripts/workflow-board-server.mjs [--port 8933];多项目并行请用 ensure-board.mjs 拉起(自动上探可用端口)
|
|
4
4
|
// 零依赖(node:http/node:fs/node:path/node:url/node:child_process);只读 workflow/ 与 git,仅绑 127.0.0.1。
|
|
5
5
|
// API:GET /(看板页) /marked.min.js(vendor) /api/board /api/doc?file=<相对路径> /api/history?file=<相对路径> /api/events(SSE)
|
|
6
|
+
// /api/runs(pipeline-run 执行器 run 卡片列表) /api/run?file=<runId>.json(单 run 事件流)
|
|
6
7
|
// 实时边界:文档内容与状态随落盘实时(fs.watch→SSE 推送);git 历史仅含已提交记录(git 语义)。
|
|
8
|
+
// 执行器面板(2026-10-04 board-run-panel):只读 .agents/cache/pipeline-runs/*.json(脚本亲写的机器事实),
|
|
9
|
+
// 不读 orchestration-runs.jsonl(宿主 AI 自报态,非验证态);纯只读观测层,不启动/推进/中止 run。
|
|
7
10
|
import http from 'node:http';
|
|
8
11
|
import fs from 'node:fs';
|
|
9
12
|
import path from 'node:path';
|
|
10
13
|
import { fileURLToPath } from 'node:url';
|
|
11
14
|
import { execFile, spawnSync } from 'node:child_process';
|
|
12
15
|
import { ENUMS } from './workflow-enums.mjs';
|
|
16
|
+
import { stageBar } from './pipeline-run.mjs';
|
|
13
17
|
|
|
14
18
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
|
15
19
|
const WORKFLOW = path.join(ROOT, 'workflow');
|
|
16
20
|
const BOARD_DIR = path.join(ROOT, '.agents', 'board');
|
|
17
21
|
const DOC_TYPES = ['intents', 'incidents', 'plans', 'specs'];
|
|
22
|
+
const PIPELINE_RUNS = path.join(ROOT, '.agents', 'cache', 'pipeline-runs');
|
|
18
23
|
|
|
19
24
|
const portArg = process.argv.indexOf('--port');
|
|
20
25
|
const PORT = portArg > 0 ? Number(process.argv[portArg + 1]) || 8933 : 8933;
|
|
@@ -53,6 +58,58 @@ function parseAcceptance(text) {
|
|
|
53
58
|
}
|
|
54
59
|
export { parseAcceptance };
|
|
55
60
|
|
|
61
|
+
// ---- 执行器 run 文件解析(2026-10-04 board-run-panel)----
|
|
62
|
+
// 只读 pipeline-runs/ 下的 run JSON(脚本亲写的机器事实,含 gate 真实退出码)。坏 JSON / 缺字段容错返回 null,
|
|
63
|
+
// 由调用方跳过——run 文件写入原子化(临时文件+rename),读瞬间可能撞上 rename 前状态,不 500。
|
|
64
|
+
export function parseRunFile(text) {
|
|
65
|
+
let j;
|
|
66
|
+
try {
|
|
67
|
+
j = JSON.parse(text);
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
if (!j || typeof j !== 'object' || typeof j.runId !== 'string' || !j.runId) return null;
|
|
72
|
+
return j;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 单 run 卡片(/api/runs 载荷元素):runId / requirement / stage / stopType / updatedAt / triage / 事件数 / 阶段条
|
|
76
|
+
export function runCard(run) {
|
|
77
|
+
const events = Array.isArray(run.events) ? run.events : [];
|
|
78
|
+
return {
|
|
79
|
+
runId: run.runId,
|
|
80
|
+
requirement: run.requirement || '',
|
|
81
|
+
stage: run.stage || '',
|
|
82
|
+
stopType: run.stopType || '',
|
|
83
|
+
updatedAt: run.updatedAt || run.createdAt || '',
|
|
84
|
+
triage: run.triage || null,
|
|
85
|
+
eventCount: events.length,
|
|
86
|
+
stageBar: stageBar(run),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 扫 pipeline-runs/ 全部 run → 卡片数组,按 updatedAt 倒序(无目录/空目录 → [])
|
|
91
|
+
export function scanRuns() {
|
|
92
|
+
if (!fs.existsSync(PIPELINE_RUNS) || !fs.statSync(PIPELINE_RUNS).isDirectory()) return [];
|
|
93
|
+
const cards = [];
|
|
94
|
+
for (const f of fs.readdirSync(PIPELINE_RUNS)) {
|
|
95
|
+
if (!f.endsWith('.json')) continue;
|
|
96
|
+
try {
|
|
97
|
+
const run = parseRunFile(fs.readFileSync(path.join(PIPELINE_RUNS, f), 'utf8'));
|
|
98
|
+
if (run) cards.push(runCard(run));
|
|
99
|
+
} catch {
|
|
100
|
+
// 读失败(瞬时占用等)跳过,等下次推送
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return cards.sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt)));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 路径白名单:resolve 后必须仍在 pipeline-runs/ 内且 .json 后缀(export 供测试直测白名单面)
|
|
107
|
+
export function safeRunPath(rel) {
|
|
108
|
+
const abs = path.resolve(PIPELINE_RUNS, rel);
|
|
109
|
+
if (!abs.startsWith(PIPELINE_RUNS + path.sep) || !abs.endsWith('.json')) return null;
|
|
110
|
+
return abs;
|
|
111
|
+
}
|
|
112
|
+
|
|
56
113
|
// ---- 配对断裂检测:按完整文件名(含日期)聚合同族,异常卡附 alerts ----
|
|
57
114
|
// 与 check-loop 检查 1 的 basename 同一口径。slug(去掉日期)只给界面分组,不参与配对。
|
|
58
115
|
// 规则(看板为预警层、不阻断;枚举读单源 workflow-enums.txt):
|
|
@@ -196,16 +253,22 @@ let debounceTimer = null;
|
|
|
196
253
|
function broadcast(event) {
|
|
197
254
|
for (const res of sseClients) { if (!res.destroyed) res.write(`event: ${event}\ndata: {}\n\n`); }
|
|
198
255
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
});
|
|
204
|
-
// watch 中途失效(目录被动/杀软锁):显式告知前端降级轮询,避免假实时
|
|
205
|
-
watcher.on('error', (e) => { console.error('fs.watch 失效,已通知前端降级轮询:', e.message); broadcast('watchdead'); });
|
|
206
|
-
} catch (e) {
|
|
207
|
-
console.error('fs.watch 初始化失败(前端将走 60s 兜底轮询):', e.message);
|
|
256
|
+
// 统一监听回调:workflow/ 文档 + pipeline-runs/ 执行器 run 文件任一变化 → 500ms 防抖广播 changed
|
|
257
|
+
function onWatchChange() {
|
|
258
|
+
clearTimeout(debounceTimer);
|
|
259
|
+
debounceTimer = setTimeout(() => broadcast('changed'), 500);
|
|
208
260
|
}
|
|
261
|
+
function watchDir(dir) {
|
|
262
|
+
try {
|
|
263
|
+
const watcher = fs.watch(dir, { recursive: true }, onWatchChange);
|
|
264
|
+
watcher.on('error', (e) => { console.error(`fs.watch 失效(${dir}),已通知前端降级轮询:`, e.message); broadcast('watchdead'); });
|
|
265
|
+
} catch (e) {
|
|
266
|
+
// 同步失败(典型:目录不存在 ENOENT)与异步 error 同口径广播 watchdead——否则前端徽标滞留「实时」而推送已死
|
|
267
|
+
console.error(`fs.watch 初始化失败(${dir},前端将走 60s 兜底轮询):`, e.message);
|
|
268
|
+
broadcast('watchdead');
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
watchDir(WORKFLOW);
|
|
209
272
|
// 25s 心跳:防半开连接;顺带清理已断开的 client
|
|
210
273
|
setInterval(() => { for (const res of sseClients) { if (res.destroyed) sseClients.delete(res); else res.write(': ping\n\n'); } }, 25000).unref();
|
|
211
274
|
|
|
@@ -269,6 +332,21 @@ const server = http.createServer(async (req, res) => {
|
|
|
269
332
|
}
|
|
270
333
|
if (url.pathname === '/api/board') return sendJson(res, 200, await scanBoard());
|
|
271
334
|
|
|
335
|
+
// 执行器面板(2026-10-04 board-run-panel):run 卡片列表 + 单 run 事件流
|
|
336
|
+
if (url.pathname === '/api/runs') return sendJson(res, 200, { cards: scanRuns() }); // 键名 cards 对齐 intent 2026-10-04-board-run-panel 验收#1
|
|
337
|
+
if (url.pathname === '/api/run') {
|
|
338
|
+
const rel = url.searchParams.get('file') || '';
|
|
339
|
+
const abs = safeRunPath(rel);
|
|
340
|
+
if (!abs) return sendJson(res, 403, { error: '路径不在 pipeline-runs/ 白名单内' });
|
|
341
|
+
try {
|
|
342
|
+
return sendJson(res, 200, { run: parseRunFile(fs.readFileSync(abs, 'utf8')) });
|
|
343
|
+
} catch (e) {
|
|
344
|
+
if (e && e.code === 'ENOENT') return sendJson(res, 404, { error: 'run 文件不存在' });
|
|
345
|
+
console.error(`读 run 失败(${rel}):`, e.message); // 权限/占用等如实 500 留痕,不并成 404
|
|
346
|
+
return sendJson(res, 500, { error: `读 run 失败:${e.message}` });
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
272
350
|
if (url.pathname === '/api/doc' || url.pathname === '/api/history') {
|
|
273
351
|
const rel = url.searchParams.get('file') || '';
|
|
274
352
|
const abs = safeDocPath(rel);
|
|
@@ -305,7 +383,11 @@ server.on('error', (e) => {
|
|
|
305
383
|
// isMain 守卫(board-kb-p1):parseLoopHardBlocks 可被测试 import(import 不 listen)
|
|
306
384
|
const isMain = process.argv[1] && process.argv[1].endsWith('workflow-board-server.mjs');
|
|
307
385
|
if (isMain) {
|
|
386
|
+
// run 目录缺失(.agents/cache/ 被 gitignore,全新装机无此目录)时 fs.watch 同步抛 ENOENT 且无重试——
|
|
387
|
+
// 补建空目录启用监听(pipeline-run 首跑本会建;失败不阻启动,由 watchDir catch 兜底广播 watchdead)
|
|
388
|
+
try { fs.mkdirSync(PIPELINE_RUNS, { recursive: true }); } catch { /* 竞态/权限等交给 watchDir 降级 */ }
|
|
389
|
+
watchDir(PIPELINE_RUNS); // 执行器 run 文件监听仅在作为服务运行时启动(import 不监听,避免测试进程挂起)
|
|
308
390
|
server.listen(PORT, '127.0.0.1', () => {
|
|
309
|
-
console.log(`workflow 看板: http://127.0.0.1:${PORT} (Ctrl+C 停止;只读 workflow
|
|
391
|
+
console.log(`workflow 看板: http://127.0.0.1:${PORT} (Ctrl+C 停止;只读 workflow/ 与 pipeline-runs/,不写任何文件;run 目录缺失时补建空目录以启用监听)`);
|
|
310
392
|
});
|
|
311
393
|
}
|
|
@@ -12,8 +12,9 @@ import os from 'node:os';
|
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
import { spawnSync } from 'node:child_process';
|
|
14
14
|
import { fileURLToPath } from 'node:url';
|
|
15
|
-
import { parseLoopHardBlocks, parseAcceptance, detectAlerts } from './workflow-board-server.mjs';
|
|
15
|
+
import { parseLoopHardBlocks, parseAcceptance, detectAlerts, parseRunFile, runCard, safeRunPath } from './workflow-board-server.mjs';
|
|
16
16
|
import { computeFingerprint } from './confirm-doc.mjs';
|
|
17
|
+
import { stageBar } from './pipeline-run.mjs';
|
|
17
18
|
|
|
18
19
|
let pass = 0;
|
|
19
20
|
let fail = 0;
|
|
@@ -161,5 +162,41 @@ const CHECK_LOOP = path.join(SCRIPT_DIR, 'check-loop.mjs');
|
|
|
161
162
|
check('⑥未知状态仍报不在枚举内', has(bogus, '状态不在枚举内'), JSON.stringify(bogus.alerts));
|
|
162
163
|
}
|
|
163
164
|
|
|
165
|
+
// ---- ⑦执行器面板纯函数(2026-10-04 board-run-panel)----
|
|
166
|
+
{
|
|
167
|
+
// 真实 run 文件形态(对齐 .agents/cache/pipeline-runs/20261002-1819-runtime-env-md-c2ju.json)
|
|
168
|
+
const real = {
|
|
169
|
+
runId: '20261002-1819-runtime-env-md-c2ju',
|
|
170
|
+
requirement: '在 runtime-env.md 补记 pipeline-run 运行态',
|
|
171
|
+
stage: 'done',
|
|
172
|
+
stopType: 'done',
|
|
173
|
+
triage: { kind: 'require', level: 'L0', module: 'pipeline', topic: 'runtime-note', decidedBy: 'ai' },
|
|
174
|
+
updatedAt: '2026-10-02T10:19:22.566Z',
|
|
175
|
+
events: [
|
|
176
|
+
{ t: '2026-10-02T10:19:02.680Z', type: 'run-created', requirement: 'x' },
|
|
177
|
+
{ t: '2026-10-02T10:19:02.682Z', type: 'work-order', id: 1, kind: 'triage' },
|
|
178
|
+
{ t: '2026-10-02T10:19:19.512Z', type: 'gate', cmd: 'git status --porcelain', exit: 0, ms: 58 },
|
|
179
|
+
{ t: '2026-10-02T10:19:22.566Z', type: 'commit', sha: '37b552a', subject: 'docs(pipeline): x' },
|
|
180
|
+
{ t: '2026-10-02T10:19:22.566Z', type: 'done' },
|
|
181
|
+
],
|
|
182
|
+
};
|
|
183
|
+
check('⑦parseRunFile:真实 run JSON 解析成功', parseRunFile(JSON.stringify(real))?.runId === real.runId);
|
|
184
|
+
check('⑦parseRunFile:坏 JSON → null', parseRunFile('{oops') === null);
|
|
185
|
+
check('⑦parseRunFile:非对象/缺 runId → null', parseRunFile('"str"') === null && parseRunFile('{}') === null && parseRunFile('{"a":1}') === null);
|
|
186
|
+
const card = runCard(real);
|
|
187
|
+
check('⑦runCard:字段映射齐全', card.runId === real.runId && card.stage === 'done' && card.stopType === 'done' && card.eventCount === 5 && card.requirement === real.requirement);
|
|
188
|
+
check('⑦runCard:updatedAt 取 updatedAt', card.updatedAt === real.updatedAt);
|
|
189
|
+
// 阶段条复用 pipeline-run stageBar(done → Plan/Design/Build 全 [✓],Test 为当前 [●])
|
|
190
|
+
check('⑦stageBar:done run 前三个阶段 [✓] 且 Test 为当前', card.stageBar.includes('[✓Plan]') && card.stageBar.includes('[✓Design]') && card.stageBar.includes('[✓Build]') && card.stageBar.includes('[●Test]'));
|
|
191
|
+
// 空事件容错
|
|
192
|
+
const empty = runCard({ runId: 'r1', requirement: '', stage: 'triage', stopType: 'work-order', events: null });
|
|
193
|
+
check('⑦runCard:events 非数组容错为 0', empty.eventCount === 0);
|
|
194
|
+
// safeRunPath 路径穿越白名单(2026-10-04 review-fix-batch:安全相关函数此前无测试;用例只用平台无关的正向斜杠相对路径)
|
|
195
|
+
check('⑦safeRunPath:合法 runId.json 通过', typeof safeRunPath('20261002-1819-x-abcd.json') === 'string' && safeRunPath('20261002-1819-x-abcd.json').endsWith('.json'));
|
|
196
|
+
check('⑦safeRunPath:.. 穿越拒绝', safeRunPath('../../package.json') === null && safeRunPath('a/../../package.json') === null);
|
|
197
|
+
check('⑦safeRunPath:根相对路径拒绝', safeRunPath('/etc/passwd') === null);
|
|
198
|
+
check('⑦safeRunPath:非 .json 拒绝', safeRunPath('20261002-1819-x-abcd.md') === null && safeRunPath('20261002-1819-x-abcd') === null);
|
|
199
|
+
}
|
|
200
|
+
|
|
164
201
|
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
165
202
|
process.exit(fail ? 1 : 0);
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
# PLAN — <与入口文档同名主题>
|
|
7
7
|
<!-- 与 intents/ 或 incidents/ 下同名入口文档配对;L2/L3 必须先有 ../specs/ 同名 spec 确认通过;AI 起草、用户对话内确认后开工 -->
|
|
8
8
|
<!-- frontmatter 受限子集(2026-09-13):状态∈draft/approved/done/superseded/cancelled(严格枚举,附注写备注键);级别∈L0/L1/L2/L3;正文不再写状态/级别行 -->
|
|
9
|
-
<!-- L1 极简形态(Quick-Plan,与 fill-plan.mjs
|
|
9
|
+
<!-- L1 极简形态(Quick-Plan,与 fill-plan.mjs 单源一致):单/少文件微改动只填「改动方案」+「约束与风险」+「验证计划」三节(L1 主节标题模板正文用「改动面」,fill-plan 生成「改动方案」——check-loop 证据校验对 改动面/任务拆解/改动方案 三种节名均识别为声明文件来源)——改动方案逐条列改哪个文件做什么(判据细节直接写进条目),验证计划一行静态门 + 按需 UI 实测;任务拆解/执行顺序仅 L1 多文件多步骤时保留 -->
|
|
10
10
|
|
|
11
11
|
对应入口:../intents/YYYY-MM-DD-<主题>.md 或 ../incidents/YYYY-MM-DD-<主题>.md(保留实际一项)
|
|
12
12
|
对应 spec:../specs/YYYY-MM-DD-<主题>.md(L1 可省略)
|