agentic-flow-kit 0.3.0 → 0.4.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 +2 -0
- package/package.json +1 -1
- package/src/doctor.mjs +15 -8
- package/templates/AGENTS.md +1 -1
- package/templates/_agents/cache/kb-index.json +1 -1
- package/templates/_agents/commands/maintain.md +1 -1
- package/templates/_agents/commands/plan.md +1 -1
- package/templates/_agents/scripts/ensure-board.mjs +158 -0
- package/templates/_agents/scripts/workflow-board-server.mjs +9 -2
- package/templates/workflow/README.md +1 -1
- package/templates/_agents/scripts/ensure-board.ps1 +0 -48
package/README.md
CHANGED
|
@@ -62,3 +62,5 @@ modules/gates/ 可选门禁模块(dotnet-ca:Clean Architecture 参考
|
|
|
62
62
|
- [x] v0.2.0 已发布:npm view agentic-flow-kit(npx agentic-flow-kit init 即用;默认镜像源用户需等 npmmirror 同步或 --registry npmjs)
|
|
63
63
|
- [x] v0.2.1:Shipyard 回流第二笔(模板引用去死链 + owned 台账记账策略:add-gate 接线刷新 + sync 盘面自愈)+ 全项目审查修复(量化台账 fail-loud / 生成器锚点与 ENOENT 守卫 / sync 跳过件持续报告 / P4 清账 / metrics glob 词表收口)——已发布
|
|
64
64
|
- [x] v0.3.0:init 交互升级(序号菜单 + ANSI 色彩降级 + ts/js 技术栈别名归一 node)+ AGENTS.md 骨架探测补齐(已存在无标记时文末追加,原内容保留)+ wiki 模板预置 7 主题内容目录 + 关单 verify 固定编排(npm test + check-loop 一键过门,非绿不关单)+ workflow 看板纸墨视觉改版
|
|
65
|
+
- [x] v0.3.1:doctor 修复——Windows 无 sh 环境(PowerShell)把 sh ENOENT 误报成 check-loop hard-block(探测分流:明示「未跑勿当作通过」,git 钩子门禁不受影响)
|
|
66
|
+
- [x] v0.4.0:看板端口自动上探(基端口起探首个可用端口,多项目并行不冲突;`/api/board` 自报 pid/startedAt,root 比对识别本项目、他人进程不动手不 kill)+ 跨平台入口 `ensure-board.mjs` 取代 Windows 专属 `ensure-board.ps1`(Windows/Linux/macOS 零依赖单入口)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentic-flow-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "AI-Native 闭环工作流 + wiki 知识层脚手架——flow-kit init 一条命令为新项目装好 .agents 工作流引擎(阶段命令/角色契约/门禁钩子/kb 检索/看板/规则面预算)+ workflow 文档闭环 + wiki 知识库,装完自包含、零运行时依赖",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
package/src/doctor.mjs
CHANGED
|
@@ -127,13 +127,20 @@ export function doctor(args, pkgRoot) {
|
|
|
127
127
|
else add('WARN', `delegations 台账结构漂移——${String(agg.stderr || agg.stdout || '').split('\n')[0]}`);
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
// 7. check-loop
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
130
|
+
// 7. check-loop——先探 sh 可用性(对齐 run-tests.mjs 先例):Windows PowerShell 常无 sh,
|
|
131
|
+
// ENOENT 曾被吞进 hard-block 分支报成空原因假警报(incident 2026-09-24-doctor-sh-enoent)。
|
|
132
|
+
// git 钩子门禁不受此影响——git 以自带 sh 执行钩子,与用户 PATH 无关。
|
|
133
|
+
const shProbe = spawnSync('sh', ['-c', 'true'], { encoding: 'utf8' });
|
|
134
|
+
if (shProbe.status !== 0) {
|
|
135
|
+
add('WARN', '环境无 sh——check-loop 未跑,勿当作通过;装 Git Bash / WSL 后重跑 doctor(git 钩子门禁不受影响,git 以自带 sh 执行)');
|
|
135
136
|
} else {
|
|
136
|
-
|
|
137
|
+
const cl = spawnSync('sh', ['.agents/scripts/check-loop.sh'], { cwd: target, encoding: 'utf8' });
|
|
138
|
+
if (cl.status === 0) {
|
|
139
|
+
const warnTxt = String(cl.stderr || '').trim();
|
|
140
|
+
add('PASS', `check-loop 干净${warnTxt ? `(${warnTxt.split('\n').filter((l) => l.includes('WARN')).length} 条 advisory 警告)` : ''}`);
|
|
141
|
+
} else {
|
|
142
|
+
add('FAIL', `check-loop 有 hard-block:\n${String(cl.stderr || '').split('\n').slice(0, 8).join('\n')}`);
|
|
143
|
+
}
|
|
137
144
|
}
|
|
138
145
|
|
|
139
146
|
// 8. 看板端口(信息级)
|
|
@@ -142,8 +149,8 @@ export function doctor(args, pkgRoot) {
|
|
|
142
149
|
portInUse(port).then((inUse) => {
|
|
143
150
|
print(results);
|
|
144
151
|
console.log(inUse
|
|
145
|
-
? `ℹ️ 看板端口 ${port} 已有监听(ensure-board
|
|
146
|
-
: `ℹ️ 看板端口 ${port} 空闲(看板默认不拉起;需要时手动 .agents/scripts/ensure-board.
|
|
152
|
+
? `ℹ️ 看板端口 ${port} 已有监听(ensure-board 探活:本项目看板复用/旧代码重启,他人进程不 kill 自动上探,见 workflow/README.md)`
|
|
153
|
+
: `ℹ️ 看板端口 ${port} 空闲(看板默认不拉起;需要时手动 .agents/scripts/ensure-board.mjs(跨平台)或 node .agents/scripts/workflow-board-server.mjs)`);
|
|
147
154
|
process.exit(results.some((r) => r.level === 'FAIL') ? 1 : 0);
|
|
148
155
|
});
|
|
149
156
|
}
|
package/templates/AGENTS.md
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
### 检索、看板与量化
|
|
32
32
|
|
|
33
33
|
- 跨语料检索:`node .agents/scripts/kb-search.mjs "<词>"`(workflow 节级定位 + wiki 全文);活跃流程读 `workflow/INDEX.md`(状态变更后 `node .agents/scripts/gen-workflow-index.mjs` 重生成)。
|
|
34
|
-
- workflow 看板(可选,默认不拉起):需要时手动跑
|
|
34
|
+
- workflow 看板(可选,默认不拉起):需要时手动跑 `node .agents/scripts/ensure-board.mjs`(跨平台,Windows/Linux/macOS 通用),端口从基端口 {{BOARD_PORT}} 起自动上探首个可用(多项目并行不冲突),链接以脚本输出为准(只读预警层,非门禁)。
|
|
35
35
|
- 委派 / 自做结果留痕 `workflow/delegations.md`(聚合 `node .agents/scripts/agg-delegations.cjs`);命令 / 技能卡壳记 `workflow/papercuts.md`,不当场顺手改。
|
|
36
36
|
|
|
37
37
|
### 验证与关单
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"fp":"b779761db958797b","files":{"/tmp/kb-search-test-
|
|
1
|
+
{"fp":"b779761db958797b","files":{"/tmp/kb-search-test-XdDPi6::workflow/intents/2026-09-21-bbb.md":{"st":[1790253784155.5151,270],"kind":"wf","meta":{"状态":"approved","级别":"L1","日期":"2026-09-21","模块":"pipeline"},"title":"乙需求","lines":[[1,"(frontmatter)","---"],[2,"(frontmatter)","状态: approved"],[3,"(frontmatter)","级别: L1"],[4,"(frontmatter)","日期: 2026-09-21"],[5,"(frontmatter)","模块: pipeline"],[6,"(frontmatter)","---"],[7,"(标题)","# INTENT — 乙需求"],[9,"目标","## 目标"],[10,"目标","- 统一口径:编码模板与分类层级,冲突处理见 A/B乙"],[11,"目标",""]]},"/tmp/kb-search-test-XdDPi6::workflow/plans/2026-09-20-ccc.md":{"st":[1790253784155.5151,200],"kind":"wf","meta":{"状态":"approved","级别":"L1","模块":"purchase"},"title":"丙计划","lines":[[1,"(frontmatter)","---"],[2,"(frontmatter)","状态: approved"],[3,"(frontmatter)","级别: L1"],[4,"(frontmatter)","模块: purchase"],[5,"(frontmatter)","---"],[6,"(标题)","# PLAN — 丙计划"],[8,"目标","## 目标"],[9,"目标","- 只讲编码模板,不提另一个词"],[10,"目标",""]]},"/tmp/kb-search-test-XdDPi6::workflow/incidents/2026-09-01-aaa.md":{"st":[1790253784155.5151,270],"kind":"wf","meta":{"状态":"closed","级别":"L2","发现":"2026-09-01","模块":"material"},"title":"甲事故","lines":[[1,"(frontmatter)","---"],[2,"(frontmatter)","状态: closed"],[3,"(frontmatter)","级别: L2"],[4,"(frontmatter)","发现: 2026-09-01"],[5,"(frontmatter)","模块: material"],[6,"(frontmatter)","---"],[7,"(标题)","# INCIDENT — 甲事故"],[9,"目标","## 目标"],[10,"目标","- 统一口径:编码模板与分类层级,冲突处理见 A/B甲"],[11,"目标",""]]},"/tmp/kb-search-test-XdDPi6::wiki/测试主题/说明.md":{"st":[1790253784155.5151,47],"kind":"wiki","lines":[[1,"# 说明"],[2,""],[3,"编码模板的领域知识条目。"],[4,""]]}}}
|
|
@@ -24,7 +24,7 @@ next: .agents/commands/plan.md(若复盘需要新 intent)
|
|
|
24
24
|
|
|
25
25
|
## 执行
|
|
26
26
|
|
|
27
|
-
> **看板(可选,默认不拉起)**:需要实时看板时手动跑
|
|
27
|
+
> **看板(可选,默认不拉起)**:需要实时看板时手动跑 node .agents/scripts/ensure-board.mjs(跨平台;幂等:探活 / 旧代码自动重启 / 全新启动才弹浏览器;多项目并行自动上探首个可用端口),链接以脚本输出为准(基端口 {{BOARD_PORT}});不开看板不影响任务流程。
|
|
28
28
|
|
|
29
29
|
### 0. 紧急止血(仍在事故中时)
|
|
30
30
|
|
|
@@ -16,7 +16,7 @@ next: L1 → .agents/commands/build.md ;L2/L3 → .agents/commands/design.md
|
|
|
16
16
|
|
|
17
17
|
## 执行
|
|
18
18
|
|
|
19
|
-
> **看板(可选,默认不拉起)**:需要实时看板时手动跑
|
|
19
|
+
> **看板(可选,默认不拉起)**:需要实时看板时手动跑 node .agents/scripts/ensure-board.mjs(跨平台;幂等:探活 / 旧代码自动重启 / 全新启动才弹浏览器;多项目并行自动上探首个可用端口),链接以脚本输出为准(基端口 {{BOARD_PORT}});不开看板不影响任务流程。
|
|
20
20
|
|
|
21
21
|
1. 若 `workflow/intents/` 已有目标 intent 且状态为 approved → 按级别跳 next(L1 → `build.md`,L2/L3 → `design.md`)
|
|
22
22
|
2. 否则起草新 intent:
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ensure-board.mjs — workflow 看板按需手动拉起(默认不自动开启;只起不关:OS 重启即天然回收,无需 stop/status)
|
|
3
|
+
// 跨平台单入口(零依赖,Windows/Linux/macOS 同一脚本,取代旧 ensure-board.ps1):
|
|
4
|
+
// 基端口(--port 显式 > kit.json boardPort > 8933)向上探测首个可用端口(窗口 10)——
|
|
5
|
+
// 端口空闲 → 全新启动;探活 /api/board 比对 root:本项目看板 → 复用(代码文件晚于服务端 startedAt = 旧代码自动 kill 重启);
|
|
6
|
+
// 他人进程(其他项目看板 / 非看板服务)不动手不 kill,跳过试下一端口;全新启动成功后弹浏览器(重启/已运行不弹,SSE 自动重连)
|
|
7
|
+
// 身份与陈旧判定全部来自 /api/board 自报(pid/startedAt)——不碰 OS 专属的端口属主 / 进程启动时间 API。
|
|
8
|
+
// 用法:node .agents/scripts/ensure-board.mjs [--port 8933] [--no-open]
|
|
9
|
+
import net from 'node:net';
|
|
10
|
+
import http from 'node:http';
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
import { spawn } from 'node:child_process';
|
|
15
|
+
|
|
16
|
+
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const ROOT = path.resolve(SCRIPT_DIR, '../..'); // 与 server 的 ROOT 同口径
|
|
18
|
+
const SERVER = path.join(SCRIPT_DIR, 'workflow-board-server.mjs');
|
|
19
|
+
const CODE_FILES = [path.join(ROOT, '.agents', 'board', 'index.html'), SERVER]; // 前端 + 服务端:任一更新都算「旧代码」
|
|
20
|
+
const WINDOW = 10;
|
|
21
|
+
|
|
22
|
+
// ---- 参数与基端口 ----
|
|
23
|
+
const argv = process.argv;
|
|
24
|
+
const flagIdx = argv.indexOf('--port');
|
|
25
|
+
const eqArg = argv.find((a) => a.startsWith('--port='));
|
|
26
|
+
const portArg = flagIdx > -1 ? argv[flagIdx + 1] : eqArg && eqArg.slice('--port='.length);
|
|
27
|
+
const noOpen = argv.includes('--no-open');
|
|
28
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
29
|
+
|
|
30
|
+
let BASE_PORT;
|
|
31
|
+
if (portArg && Number(portArg)) {
|
|
32
|
+
BASE_PORT = Number(portArg);
|
|
33
|
+
} else {
|
|
34
|
+
// 基端口取 kit 台账(init 询问值,与命令文档 {{BOARD_PORT}} 同源);无台账或解析失败回落 8933
|
|
35
|
+
try { BASE_PORT = Number(JSON.parse(fs.readFileSync(path.join(ROOT, '.agents', 'kit.json'), 'utf8')).options.boardPort) || 8933; }
|
|
36
|
+
catch { BASE_PORT = 8933; }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const samePath = (a, b) => (process.platform === 'win32' ? a.toLowerCase() === b.toLowerCase() : a === b);
|
|
40
|
+
|
|
41
|
+
// ---- 探活:端口占用用 net.connect;看板身份/属主用 /api/board 自报(isBoard=含 cards 字段;ours=root 指向本项目)----
|
|
42
|
+
// 不用全局 fetch:undici 连接池 keep-alive 句柄会拖住/污染退出(Windows 上强退触发 libuv 断言)
|
|
43
|
+
function portBusy(port) {
|
|
44
|
+
return new Promise((resolve) => {
|
|
45
|
+
const s = net.connect({ host: '127.0.0.1', port });
|
|
46
|
+
s.setTimeout(800);
|
|
47
|
+
s.on('connect', () => { s.destroy(); resolve(true); });
|
|
48
|
+
s.on('timeout', () => { s.destroy(); resolve(true); }); // 超时从保守视作占用;localhost 上近乎不存在
|
|
49
|
+
s.on('error', () => resolve(false));
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// /api/board 用 http.get 正规解包(server 响应为 chunked,手工拼包解析不了);Connection: close + 默认 agent 不进连接池,无残留句柄
|
|
54
|
+
function httpProbe(port) {
|
|
55
|
+
return new Promise((resolve) => {
|
|
56
|
+
const req = http.get({ host: '127.0.0.1', port, path: '/api/board', headers: { Connection: 'close' }, timeout: 2000 }, (res) => {
|
|
57
|
+
let buf = '';
|
|
58
|
+
res.on('data', (d) => { buf += d; });
|
|
59
|
+
res.on('end', () => resolve({ status: res.statusCode, json: () => { try { return JSON.parse(buf); } catch { return null; } } }));
|
|
60
|
+
});
|
|
61
|
+
req.on('timeout', () => { req.destroy(); resolve({ status: 0 }); });
|
|
62
|
+
req.on('error', () => resolve({ status: 0 }));
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function probeBoard(port) {
|
|
67
|
+
const none = { isBoard: false };
|
|
68
|
+
const r = await httpProbe(port);
|
|
69
|
+
if (r.status !== 200) return none;
|
|
70
|
+
const j = r.json();
|
|
71
|
+
if (!j || typeof j !== 'object' || !('cards' in j)) return none;
|
|
72
|
+
return { isBoard: true, ours: !!(j.root && samePath(path.resolve(String(j.root)), ROOT)), pid: j.pid, startedAt: j.startedAt };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 陈旧判定:代码文件最新 mtime 晚于服务端启动时刻(2s 容差)= 旧代码;无 startedAt(旧版服务端)返回 null 无法判定
|
|
76
|
+
function staleCode(startedAt) {
|
|
77
|
+
if (!startedAt) return null;
|
|
78
|
+
const newest = Math.max(...CODE_FILES.filter((f) => fs.existsSync(f)).map((f) => fs.statSync(f).mtimeMs));
|
|
79
|
+
return newest > startedAt + 2000;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ---- 动作:kill / 拉起(detached + windowsHide,父进程退出后存活)/ 等监听 / 弹浏览器 ----
|
|
83
|
+
function startServer(port) {
|
|
84
|
+
const c = spawn(process.execPath, [SERVER, '--port', String(port)], { detached: true, stdio: 'ignore', windowsHide: true });
|
|
85
|
+
c.unref();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function waitListen(port) {
|
|
89
|
+
const deadline = Date.now() + 8000;
|
|
90
|
+
while (Date.now() < deadline) {
|
|
91
|
+
if (await portBusy(port)) return true;
|
|
92
|
+
await sleep(300);
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// intentional-simple: 弹浏览器 best-effort 三平台分流(win: cmd start / mac: open / linux: xdg-open),失败不打扰——链接已在输出里
|
|
98
|
+
function openBrowser(url) {
|
|
99
|
+
try {
|
|
100
|
+
if (process.platform === 'win32') spawn('cmd', ['/c', 'start', '', url], { detached: true, stdio: 'ignore', windowsHide: true }).unref();
|
|
101
|
+
else spawn(process.platform === 'darwin' ? 'open' : 'xdg-open', [url], { detached: true, stdio: 'ignore' }).unref();
|
|
102
|
+
} catch { }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---- 主流程(返回退出码,自然排水退出——不用 process.exit:强退在 Windows 上会触发 libuv 断言)----
|
|
106
|
+
async function main() {
|
|
107
|
+
const skipped = [];
|
|
108
|
+
let action = null;
|
|
109
|
+
let port = 0;
|
|
110
|
+
let board = null;
|
|
111
|
+
|
|
112
|
+
for (let p = BASE_PORT; p < BASE_PORT + WINDOW; p++) {
|
|
113
|
+
if (!(await portBusy(p))) { port = p; action = 'fresh start'; break; }
|
|
114
|
+
const b = await probeBoard(p);
|
|
115
|
+
if (b.ours) {
|
|
116
|
+
port = p;
|
|
117
|
+
board = b;
|
|
118
|
+
const stale = staleCode(b.startedAt);
|
|
119
|
+
if (stale === true) {
|
|
120
|
+
try { process.kill(b.pid); } catch { } // 进程可能已自行退出,忽略
|
|
121
|
+
await sleep(500);
|
|
122
|
+
action = 'restarted (stale code detected)';
|
|
123
|
+
} else if (stale === null) {
|
|
124
|
+
action = 'legacy';
|
|
125
|
+
} else {
|
|
126
|
+
action = 'up-to-date';
|
|
127
|
+
}
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
skipped.push(`端口 ${p}(pid ${b.pid ?? '未知'})`);
|
|
131
|
+
}
|
|
132
|
+
const url = `http://127.0.0.1:${port}`;
|
|
133
|
+
|
|
134
|
+
if (!action) {
|
|
135
|
+
console.log(`board: 端口 ${BASE_PORT}..${BASE_PORT + WINDOW - 1} 均被他人进程占用(未动手不 kill):${skipped.join(';')};可 --port 指定其他基端口`);
|
|
136
|
+
return 1;
|
|
137
|
+
}
|
|
138
|
+
if (action === 'legacy') {
|
|
139
|
+
console.log(`board: ${url} 是本项目旧版看板(无 startedAt,无法判定代码新旧),未自动重启;如需更新请手动重启`);
|
|
140
|
+
return 0;
|
|
141
|
+
}
|
|
142
|
+
if (action === 'up-to-date') {
|
|
143
|
+
console.log(`board: already up-to-date at ${url} (pid ${board.pid ?? '未知'})`);
|
|
144
|
+
return 0;
|
|
145
|
+
}
|
|
146
|
+
if (skipped.length) console.log(`board: 基端口段他人进程已跳过:${skipped.join(';')}`);
|
|
147
|
+
|
|
148
|
+
startServer(port);
|
|
149
|
+
if (!(await waitListen(port))) {
|
|
150
|
+
console.log(`board: FAILED to start on ${port}`);
|
|
151
|
+
return 1;
|
|
152
|
+
}
|
|
153
|
+
console.log(`board: ${action} -> ${url}`);
|
|
154
|
+
if (!noOpen && action === 'fresh start') openBrowser(url);
|
|
155
|
+
return 0;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
process.exitCode = await main();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// workflow 实时看板服务:本地只读 http + SSE 文件监听,可视化 AI 执行流程(intents/incidents/plans/specs)。
|
|
3
|
-
// 用法:node .agents/scripts/workflow-board-server.mjs [--port 8933]
|
|
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
6
|
// 实时边界:文档内容与状态随落盘实时(fs.watch→SSE 推送);git 历史仅含已提交记录(git 语义)。
|
|
@@ -17,6 +17,7 @@ const DOC_TYPES = ['intents', 'incidents', 'plans', 'specs'];
|
|
|
17
17
|
|
|
18
18
|
const portArg = process.argv.indexOf('--port');
|
|
19
19
|
const PORT = portArg > 0 ? Number(process.argv[portArg + 1]) || 8933 : 8933;
|
|
20
|
+
const STARTED_AT = Date.now(); // 随 /api/board 自报(startedAt),供 ensure-board.mjs 判定旧代码重启,不碰进程启动时间 API
|
|
20
21
|
|
|
21
22
|
// ---- front matter + 标题解析(对照 _TEMPLATE.md 受限子集:每行 `键: 值`)----
|
|
22
23
|
function parseDoc(text) {
|
|
@@ -155,7 +156,7 @@ async function scanBoard() {
|
|
|
155
156
|
}
|
|
156
157
|
detectAlerts(cards);
|
|
157
158
|
cards.sort((a, b) => b.date.localeCompare(a.date) || a.type.localeCompare(b.type));
|
|
158
|
-
return { root: ROOT, counts, cards };
|
|
159
|
+
return { root: ROOT, pid: process.pid, startedAt: STARTED_AT, counts, cards };
|
|
159
160
|
}
|
|
160
161
|
|
|
161
162
|
// ---- 路径白名单:resolve 后必须仍在 workflow/ 内 ----
|
|
@@ -240,6 +241,12 @@ const server = http.createServer(async (req, res) => {
|
|
|
240
241
|
}
|
|
241
242
|
});
|
|
242
243
|
|
|
244
|
+
// 绑定失败明确退出(EADDRINUSE 常见于直跑撞端口;ensure-board 会先探活选端口,这里兜底可读报错)
|
|
245
|
+
server.on('error', (e) => {
|
|
246
|
+
console.error(`workflow 看板启动失败(端口 ${PORT}${e.code === 'EADDRINUSE' ? ' 已被占用' : ''}):${e.code || e.message}`);
|
|
247
|
+
process.exit(1);
|
|
248
|
+
});
|
|
249
|
+
|
|
243
250
|
server.listen(PORT, '127.0.0.1', () => {
|
|
244
251
|
console.log(`workflow 看板: http://127.0.0.1:${PORT} (Ctrl+C 停止;只读 workflow/,不写任何文件)`);
|
|
245
252
|
});
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
## 使用方式
|
|
25
25
|
|
|
26
26
|
- **发起新任务**:执行 `.agents/commands/new-task.md` 定义的流程(6 阶段总入口)
|
|
27
|
-
- **看板(可选,默认不拉起)**:需要时手动跑 .agents/scripts/ensure-board.
|
|
27
|
+
- **看板(可选,默认不拉起)**:需要时手动跑 node .agents/scripts/ensure-board.mjs(跨平台单入口;幂等:探活 / 旧代码自动重启 / 全新启动弹浏览器),端口从基端口 {{BOARD_PORT}} 起自动上探首个可用,链接以脚本输出为准;**预警层,非门禁;告警口径对齐 check-loop 硬断档**。端口被占时探活 `/api/board` 并比对 `root`——本项目看板才复用 / 旧代码重启;他人进程(含其他项目看板)不动手不 kill,自动跳过试下一端口
|
|
28
28
|
- **检索**:活跃流程读 `INDEX.md`(生成物,`node .agents/scripts/gen-workflow-index.mjs` 重生成、`--check` 校验漂移);跨语料检索 `node .agents/scripts/kb-search.mjs "<词>"`(workflow 按节级定位 + wiki 全文,`--type/--module/--status/-n` 过滤;`--status all` 显式全量)
|
|
29
29
|
- **验证**:`.agents/commands/test.md`(静态门 + 实测;项目自有验证脚本/门禁如有,见 `.agents/hooks/local-pre-commit` 与根 `AGENTS.md`「项目适配区」)
|
|
30
30
|
- **评审**:`.agents/commands/review.md`(按 P0 / P1 / P2 分级:机器兜底 + AI 自查出清单,用户决策定性与合入)
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
# ensure-board.ps1 — workflow 看板按需手动拉起(默认不自动开启;只起不关:Windows 重启即天然回收,无需 stop/status)
|
|
2
|
-
# 行为:探活(端口占用者须响应 /api/board 才认定为看板,否则不动手、报占用者并 exit 1)
|
|
3
|
-
# → 前端或服务端任一代码早于进程 = 旧代码自动 kill 重启 → 全新启动成功后弹浏览器(重启/已运行不弹,SSE 自动重连)
|
|
4
|
-
# 用法:powershell -NoProfile -File .agents/scripts/ensure-board.ps1 [-Port 8933] [-NoOpen]
|
|
5
|
-
param(
|
|
6
|
-
[int]$Port = 8933,
|
|
7
|
-
[switch]$NoOpen
|
|
8
|
-
)
|
|
9
|
-
$ErrorActionPreference = 'Stop'
|
|
10
|
-
$agentsDir = Split-Path -Parent $PSScriptRoot
|
|
11
|
-
$server = Join-Path $PSScriptRoot 'workflow-board-server.mjs'
|
|
12
|
-
$boardHtml = Join-Path $agentsDir 'board\index.html'
|
|
13
|
-
$codeFiles = @($boardHtml, $server) # 前端 + 服务端:任一更新都算「旧代码」
|
|
14
|
-
$url = "http://127.0.0.1:$Port"
|
|
15
|
-
|
|
16
|
-
$proc = $null
|
|
17
|
-
$conn = Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
18
|
-
if ($conn) { $proc = Get-Process -Id $conn.OwningProcess -ErrorAction SilentlyContinue }
|
|
19
|
-
|
|
20
|
-
$action = 'fresh start'
|
|
21
|
-
if ($proc) {
|
|
22
|
-
# 身份校验:只有能返回带 cards 字段 JSON 的才是看板,否则绝不 kill(可能误杀用户自己的服务)
|
|
23
|
-
$isBoard = $false
|
|
24
|
-
try {
|
|
25
|
-
$body = (Invoke-WebRequest -Uri "$url/api/board" -TimeoutSec 2).Content
|
|
26
|
-
$isBoard = $null -ne ($body | ConvertFrom-Json).PSObject.Properties['cards']
|
|
27
|
-
} catch { $isBoard = $false }
|
|
28
|
-
if (-not $isBoard) {
|
|
29
|
-
Write-Output "board: 端口 $Port 被非看板进程占用(pid $($proc.Id) $($proc.ProcessName)),未做任何改动;请确认该进程后自行处理"
|
|
30
|
-
exit 1
|
|
31
|
-
}
|
|
32
|
-
$newest = ($codeFiles | ForEach-Object { (Get-Item $_).LastWriteTime } | Sort-Object -Descending)[0]
|
|
33
|
-
$stale = $newest -gt $proc.StartTime.AddSeconds(2)
|
|
34
|
-
if (-not $stale) {
|
|
35
|
-
Write-Output "board: already up-to-date at $url (pid $($proc.Id))"
|
|
36
|
-
exit 0
|
|
37
|
-
}
|
|
38
|
-
Stop-Process -Id $proc.Id -Force
|
|
39
|
-
Start-Sleep -Milliseconds 500
|
|
40
|
-
$action = 'restarted (stale code detected)'
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
Start-Process -FilePath 'node' -ArgumentList "`"$server`" --port $Port" -WindowStyle Hidden
|
|
44
|
-
$deadline = (Get-Date).AddSeconds(8)
|
|
45
|
-
do { Start-Sleep -Milliseconds 300; $up = [bool](Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue) } while (-not $up -and (Get-Date) -lt $deadline)
|
|
46
|
-
if (-not $up) { Write-Output "board: FAILED to start on $Port"; exit 1 }
|
|
47
|
-
Write-Output "board: $action -> $url"
|
|
48
|
-
if (-not $NoOpen -and $action -eq 'fresh start') { Start-Process $url }
|