dsh-gate-agent 0.1.4 → 0.1.5
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 +190 -4
- package/lib/index.js +305 -31
- package/package.json +1 -1
- package/skills/gate-handoff/SKILL.md +1 -1
- package/skills/gate-playwright-e2e/SKILL.md +4 -0
- package/skills/gate-prd/SKILL.md +1 -1
- package/skills/gate-research/SKILL.md +3 -3
- package/skills/gate-writing-for-agents/SKILL.md +2 -0
package/README.md
CHANGED
|
@@ -72,7 +72,7 @@ Host 插件:用 phase/node 门禁组织讨论、规划、实施、评审、验
|
|
|
72
72
|
| `reviewing/code-review-standalone` | 单独只读 code review | review worker;`gate-code-review` |
|
|
73
73
|
| `reviewing/proposal-review` | 单独提案评审 | review worker;`gate-plan-review` |
|
|
74
74
|
| `reviewing/verify-standalone` | 单独只读验收 | verify worker;`gate-user-verify` |
|
|
75
|
-
| `docs` | 文档治理 | root
|
|
75
|
+
| `docs` | 文档治理 | root;按项目文档规范处理 |
|
|
76
76
|
| `blocked` | 冻结写入/派 worker,等待用户决定 | root;`/gate-resume` |
|
|
77
77
|
| `complete` | 工作完成 | root;可重新进入 plan/implement |
|
|
78
78
|
|
|
@@ -111,7 +111,8 @@ Host 插件:用 phase/node 门禁组织讨论、规划、实施、评审、验
|
|
|
111
111
|
| `/gate-init` | 无 | 初始化 notes、AGENTS.md、`verify-notes`、CI、git 与基础 `.gitignore`,保持 `discuss` |
|
|
112
112
|
| `/gate-enter-plan [具体工作事项或上下文语义]` | 可选 | 基于讨论共识、既有决策与用户输入进入 `orchestrating/plan` |
|
|
113
113
|
| `/gate-enter-implement [具体工作事项或上下文语义]` | 可选 | 基于已确认计划或讨论后的实施说明进入 `orchestrating/implement` |
|
|
114
|
-
| `/gate-
|
|
114
|
+
| `/gate-prd [主题或上下文语义]` | 可选 | 用户级创建/更新 PRD;仅 discuss |
|
|
115
|
+
| `/gate-docs [文档范围或上下文语义]` | 可选 | 进入文档治理,按项目文档规范处理 |
|
|
115
116
|
| `/gate-implement-review [具体工作事项或上下文语义]` | 可选 | 对当前改动做独立只读 code review,只输出 findings |
|
|
116
117
|
| `/gate-code-verify [具体工作事项或上下文语义]` | 可选 | 对当前改动做独立只读验收(gate-user-verify) |
|
|
117
118
|
| `/gate-fix-bug [具体工作事项或上下文语义]` | 可选 | 基于描述或当前上下文派 fix worker 修复已授权缺陷 |
|
|
@@ -245,7 +246,7 @@ review worker 使用 `gate-plan-review` 做 Standards/Spec/Architecture 三轴
|
|
|
245
246
|
/gate-docs
|
|
246
247
|
```
|
|
247
248
|
|
|
248
|
-
进入 `docs` phase
|
|
249
|
+
进入 `docs` phase,由主代理按项目文档规范处理。
|
|
249
250
|
|
|
250
251
|
### 暂停、恢复与讨论
|
|
251
252
|
|
|
@@ -306,6 +307,14 @@ gate_record({
|
|
|
306
307
|
gate_status()
|
|
307
308
|
```
|
|
308
309
|
|
|
310
|
+
### gate_artifact_write
|
|
311
|
+
|
|
312
|
+
按 `.agents/gate/artifact-policy.json` 写白名单 artifact;只写文件,不走普通 workspace write。支持精确文件、目录和 glob。
|
|
313
|
+
|
|
314
|
+
### gate_research_dispatch
|
|
315
|
+
|
|
316
|
+
root-only;在 discuss 或 `orchestrating/plan` 派 research worker。worker 只能通过 artifact policy 写 research 文档,不获得通用 workspace 写。
|
|
317
|
+
|
|
309
318
|
### Note 工具
|
|
310
319
|
|
|
311
320
|
- `gate_note_write`:planning worker 写 proposed Note,或原地更新 implemented Note;通用 `write`/`edit`/`apply_patch` 和仓库写被拒绝,只读 shell 可用。
|
|
@@ -334,7 +343,183 @@ gate_status()
|
|
|
334
343
|
|
|
335
344
|
## 配置
|
|
336
345
|
|
|
337
|
-
|
|
346
|
+
### artifact-policy.json
|
|
347
|
+
|
|
348
|
+
discuss 和 plan 默认不允许普通文件写。`handoff`、PRD、research 等产物通过**独立的 artifact 白名单**写入,而不是放开通用 `write`。
|
|
349
|
+
|
|
350
|
+
`/gate-init` 会创建:
|
|
351
|
+
|
|
352
|
+
```text
|
|
353
|
+
.agents/gate/artifact-policy.json
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
- 文件不存在时写入基础配置;
|
|
357
|
+
- 文件已存在时**不覆盖**,保留用户内容;
|
|
358
|
+
- 新建 `.gitignore` 时包含 `.scratch/`;已有 `.gitignore` 不重写。
|
|
359
|
+
|
|
360
|
+
#### 基础配置
|
|
361
|
+
|
|
362
|
+
```json
|
|
363
|
+
{
|
|
364
|
+
"version": 1,
|
|
365
|
+
"artifacts": {
|
|
366
|
+
"handoff": {
|
|
367
|
+
"phases": ["*"],
|
|
368
|
+
"roles": ["root"],
|
|
369
|
+
"paths": [".scratch/handoff/**"]
|
|
370
|
+
},
|
|
371
|
+
"prd": {
|
|
372
|
+
"phases": ["discuss"],
|
|
373
|
+
"roles": ["root"],
|
|
374
|
+
"paths": ["docs/prd/**", ".scratch/prd/**"]
|
|
375
|
+
},
|
|
376
|
+
"research": {
|
|
377
|
+
"phases": ["discuss", "orchestrating/plan"],
|
|
378
|
+
"roles": ["root", "worker"],
|
|
379
|
+
"paths": ["docs/research/**", ".scratch/research/**"]
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
#### 字段
|
|
386
|
+
|
|
387
|
+
| 字段 | 说明 |
|
|
388
|
+
|---|---|
|
|
389
|
+
| `version` | 必须为 `1` |
|
|
390
|
+
| `artifacts` | artifact kind 到规则的映射 |
|
|
391
|
+
| `kind` | 内置 `handoff`、`prd`、`research`;也可以增加自定义 kind |
|
|
392
|
+
| `phases` | 允许的 canonical phase/node key,或 `*` |
|
|
393
|
+
| `roles` | `root` 或 `worker` |
|
|
394
|
+
| `paths` | 相对 workspace 的文件、目录或 glob |
|
|
395
|
+
|
|
396
|
+
#### phases
|
|
397
|
+
|
|
398
|
+
使用 `gate_status` 返回的 `methodologyKey` 形式:
|
|
399
|
+
|
|
400
|
+
- `discuss`
|
|
401
|
+
- `docs`
|
|
402
|
+
- `complete`
|
|
403
|
+
- `orchestrating/plan`
|
|
404
|
+
- `orchestrating/plan-review`
|
|
405
|
+
- `orchestrating/plan-revise`
|
|
406
|
+
- `orchestrating/implement`
|
|
407
|
+
- `orchestrating/code-review`
|
|
408
|
+
- `orchestrating/implement-revise`
|
|
409
|
+
- `orchestrating/verify`
|
|
410
|
+
- `orchestrating/note-lifecycle`
|
|
411
|
+
- `orchestrating/fix-bug`
|
|
412
|
+
- `reviewing/code-review-standalone`
|
|
413
|
+
- `reviewing/proposal-review`
|
|
414
|
+
- `reviewing/verify-standalone`
|
|
415
|
+
|
|
416
|
+
`*` 表示任意**非 blocked** 状态。`blocked` 始终拒绝,即使规则写了 `*`。
|
|
417
|
+
|
|
418
|
+
#### roles
|
|
419
|
+
|
|
420
|
+
- `root`:主代理;
|
|
421
|
+
- `worker`:任何 gate worker,包括 planning worker 和 research worker。
|
|
422
|
+
|
|
423
|
+
#### paths
|
|
424
|
+
|
|
425
|
+
支持四种形式:
|
|
426
|
+
|
|
427
|
+
```json
|
|
428
|
+
"paths": [
|
|
429
|
+
"docs/prd/current.md",
|
|
430
|
+
"docs/prd/PRD-*.md",
|
|
431
|
+
".scratch/research/**/*.md",
|
|
432
|
+
".scratch/handoff/**"
|
|
433
|
+
]
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
| 形式 | 含义 |
|
|
437
|
+
|---|---|
|
|
438
|
+
| `docs/prd/current.md` | 只允许这个文件 |
|
|
439
|
+
| `docs/prd/*.md` | 只允许该目录下直接文件,不递归 |
|
|
440
|
+
| `.scratch/research/**/*.md` | 递归匹配 Markdown |
|
|
441
|
+
| `.scratch/handoff/**` | 允许该目录下任意层级文件 |
|
|
442
|
+
| `docs/prd/**` | 允许该目录下任意层级文件 |
|
|
443
|
+
|
|
444
|
+
规则:
|
|
445
|
+
|
|
446
|
+
- `*` 只匹配一个路径段;
|
|
447
|
+
- `**` 匹配零个或多个路径段;
|
|
448
|
+
- 普通路径没有通配符时按**明确文件**处理;
|
|
449
|
+
- 不允许绝对路径、`..`、workspace 外路径、symlink;
|
|
450
|
+
- 保留路径永远拒绝:
|
|
451
|
+
- `.agents/gate/artifact-policy.json`
|
|
452
|
+
- `.agents/notes/**`
|
|
453
|
+
- 只写文件;父目录可以由工具创建,但也要通过边界检查。
|
|
454
|
+
|
|
455
|
+
#### 如何按项目填写
|
|
456
|
+
|
|
457
|
+
只允许一个固定 PRD 文件:
|
|
458
|
+
|
|
459
|
+
```json
|
|
460
|
+
{
|
|
461
|
+
"version": 1,
|
|
462
|
+
"artifacts": {
|
|
463
|
+
"prd": {
|
|
464
|
+
"phases": ["discuss"],
|
|
465
|
+
"roles": ["root"],
|
|
466
|
+
"paths": ["docs/prd/current.md"]
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
允许按命名规则新建多个 PRD:
|
|
473
|
+
|
|
474
|
+
```json
|
|
475
|
+
{
|
|
476
|
+
"version": 1,
|
|
477
|
+
"artifacts": {
|
|
478
|
+
"prd": {
|
|
479
|
+
"phases": ["discuss"],
|
|
480
|
+
"roles": ["root"],
|
|
481
|
+
"paths": ["docs/prd/PRD-*.md"]
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
允许 research 在 discuss 和 plan 写文档目录:
|
|
488
|
+
|
|
489
|
+
```json
|
|
490
|
+
{
|
|
491
|
+
"version": 1,
|
|
492
|
+
"artifacts": {
|
|
493
|
+
"research": {
|
|
494
|
+
"phases": ["discuss", "orchestrating/plan"],
|
|
495
|
+
"roles": ["root", "worker"],
|
|
496
|
+
"paths": ["docs/research/**/*.md", ".scratch/research/**"]
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
#### 相关工具与命令
|
|
503
|
+
|
|
504
|
+
- `gate_artifact_write({ kind, path, content })`:root/worker 按 policy 写文件;
|
|
505
|
+
- `gate_research_dispatch({ question, purpose? })`:root-only,在 discuss/plan 派 research worker;
|
|
506
|
+
- `/gate-prd [主题]`:discuss 用户命令,加载 `gate-prd` skill 并通过 artifact policy 写 PRD;
|
|
507
|
+
- `gate-handoff`:用户级 skill,产物写入 `.scratch/handoff/`。
|
|
508
|
+
|
|
509
|
+
普通 `write`/`edit`/`apply_patch`/写型 shell 不受 artifact policy 放行,仍按 phase/role 矩阵处理。
|
|
510
|
+
|
|
511
|
+
#### 常见失败
|
|
512
|
+
|
|
513
|
+
| 错误 | 原因 |
|
|
514
|
+
|---|---|
|
|
515
|
+
| `artifact-policy-invalid` | policy 缺失、JSON 无效、version 不是 1、字段非法 |
|
|
516
|
+
| `artifact-denied` | kind 不存在、phase 不匹配、role 不匹配、path 不在白名单、blocked、保留路径、越界或 symlink |
|
|
517
|
+
| `subagents-unavailable` | research dispatch 时 `ctx.subagents` 不可用 |
|
|
518
|
+
| `wrong-phase` | research dispatch 不在 discuss 或 `orchestrating/plan` |
|
|
519
|
+
|
|
520
|
+
修改 policy 后不需要重启 DSH;每次 artifact 写入都会重新读取文件。
|
|
521
|
+
|
|
522
|
+
### 插件 Config
|
|
338
523
|
|
|
339
524
|
```json
|
|
340
525
|
{ "reviewLimit": 5 }
|
|
@@ -342,6 +527,7 @@ gate_status()
|
|
|
342
527
|
|
|
343
528
|
`reviewLimit` 是每个 review 节点的最大轮次;默认 5,范围 1–20。达到上限仍有 findings 时进入 blocked。
|
|
344
529
|
|
|
530
|
+
|
|
345
531
|
## 使用边界
|
|
346
532
|
|
|
347
533
|
- gate 是流程门禁,不是 OS 安全边界;未知工具和任意 shell 语义不会被完整分析。
|
package/lib/index.js
CHANGED
|
@@ -126,7 +126,7 @@ var FIX_BUG_ORCHESTRATION = `## /gate-fix-bug \u7F16\u6392
|
|
|
126
126
|
## Step 4 \u2014 Record
|
|
127
127
|
\u5B8C\u6210\u540E\u8C03 gate_record({node:'fix-bug', workerSessionIds})\uFF0CworkerSessionIds \u5305\u542B fix \u4E0E review worker\uFF1Bgate \u4E0D\u518D\u81EA\u52A8\u63D0\u4EA4\u3002`;
|
|
128
128
|
var DOCS_ORCHESTRATION = `## /gate-docs \u7F16\u6392
|
|
129
|
-
\
|
|
129
|
+
\u8303\u56F4\u7531\u6A21\u578B\u6309\u9879\u76EE\u5B9E\u9645\u6587\u6863\u9700\u8981\u5224\u65AD\u3002
|
|
130
130
|
\u5B8C\u6210\u540E\u8C03 gate_record({node:'docs'}) \u56DE\u5230 discuss\u3002`;
|
|
131
131
|
var NOTE_LIFECYCLE_ORCHESTRATION = `## Note lifecycle
|
|
132
132
|
Verify \u5DF2 PASS\uFF1B\u5F53\u524D\u4E0D\u6D3E worker\uFF0C\u4E5F\u4E0D\u5199\u4E1A\u52A1\u4EE3\u7801\u3002
|
|
@@ -1365,7 +1365,7 @@ function installRestrictions(ctx, deps) {
|
|
|
1365
1365
|
}
|
|
1366
1366
|
|
|
1367
1367
|
// src/commands.ts
|
|
1368
|
-
import
|
|
1368
|
+
import path10 from "node:path";
|
|
1369
1369
|
|
|
1370
1370
|
// src/notes/provision.ts
|
|
1371
1371
|
import { copyFileSync, existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
@@ -1439,6 +1439,7 @@ var BASE_GITIGNORE = `node_modules/
|
|
|
1439
1439
|
Thumbs.db
|
|
1440
1440
|
coverage/
|
|
1441
1441
|
dist/
|
|
1442
|
+
.scratch/
|
|
1442
1443
|
.agents/notes/.gate-finalize-*
|
|
1443
1444
|
.agents/notes/.gate-validate-*
|
|
1444
1445
|
.agents/notes/*.gate-backup
|
|
@@ -1486,16 +1487,152 @@ function provisionCi(root) {
|
|
|
1486
1487
|
return { workflow: path8.relative(root, workflow).replaceAll("\\", "/"), scripts: path8.relative(root, ciDir).replaceAll("\\", "/"), changed: !existing };
|
|
1487
1488
|
}
|
|
1488
1489
|
|
|
1490
|
+
// src/artifacts/policy.ts
|
|
1491
|
+
import { existsSync as existsSync4, lstatSync as lstatSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
1492
|
+
import path9 from "node:path";
|
|
1493
|
+
var ARTIFACT_POLICY_PATH = ".agents/gate/artifact-policy.json";
|
|
1494
|
+
var BASE_ARTIFACT_POLICY = {
|
|
1495
|
+
version: 1,
|
|
1496
|
+
artifacts: {
|
|
1497
|
+
handoff: { phases: ["*"], roles: ["root"], paths: [".scratch/handoff/**"] },
|
|
1498
|
+
prd: { phases: ["discuss"], roles: ["root"], paths: ["docs/prd/**", ".scratch/prd/**"] },
|
|
1499
|
+
research: {
|
|
1500
|
+
phases: ["discuss", "orchestrating/plan"],
|
|
1501
|
+
roles: ["root", "worker"],
|
|
1502
|
+
paths: ["docs/research/**", ".scratch/research/**"]
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
};
|
|
1506
|
+
var PHASES2 = new Set(METHODOLOGY_KEYS);
|
|
1507
|
+
var RESERVED = [".agents/gate/artifact-policy.json", ".agents/notes/"];
|
|
1508
|
+
function normalizeArtifactPath(value) {
|
|
1509
|
+
return path9.posix.normalize(String(value).replaceAll("\\", "/").replace(/^\.\//u, ""));
|
|
1510
|
+
}
|
|
1511
|
+
function isReservedPath(value) {
|
|
1512
|
+
const normalized = value.toLowerCase();
|
|
1513
|
+
return RESERVED.some((prefix) => normalized === prefix || normalized.startsWith(prefix.endsWith("/") ? prefix : `${prefix}/`));
|
|
1514
|
+
}
|
|
1515
|
+
function artifactPolicyText(policy = BASE_ARTIFACT_POLICY) {
|
|
1516
|
+
return `${JSON.stringify(policy, null, 2)}
|
|
1517
|
+
`;
|
|
1518
|
+
}
|
|
1519
|
+
function provisionArtifactPolicy(root) {
|
|
1520
|
+
const file = path9.join(root, ARTIFACT_POLICY_PATH);
|
|
1521
|
+
if (pathHasSymlink(root, file)) throw new Error("artifact policy \u8DEF\u5F84\u5305\u542B symlink");
|
|
1522
|
+
mkdirSync4(path9.dirname(file), { recursive: true });
|
|
1523
|
+
if (existsSync4(file)) return { path: ARTIFACT_POLICY_PATH, status: "existing" };
|
|
1524
|
+
writeFileSync4(file, artifactPolicyText());
|
|
1525
|
+
return { path: ARTIFACT_POLICY_PATH, status: "created" };
|
|
1526
|
+
}
|
|
1527
|
+
function parseArtifactPolicy(value) {
|
|
1528
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return { error: "artifact policy \u5FC5\u987B\u662F\u5BF9\u8C61" };
|
|
1529
|
+
const source = value;
|
|
1530
|
+
if (source.version !== 1) return { error: "artifact policy version \u5FC5\u987B\u4E3A 1" };
|
|
1531
|
+
if (source.artifacts === null || typeof source.artifacts !== "object" || Array.isArray(source.artifacts)) {
|
|
1532
|
+
return { error: "artifact policy artifacts \u5FC5\u987B\u662F\u5BF9\u8C61" };
|
|
1533
|
+
}
|
|
1534
|
+
for (const key of Object.keys(source)) {
|
|
1535
|
+
if (key !== "version" && key !== "artifacts") return { error: `artifact policy \u672A\u77E5\u5B57\u6BB5\uFF1A${key}` };
|
|
1536
|
+
}
|
|
1537
|
+
const artifacts = {};
|
|
1538
|
+
for (const [kind, raw] of Object.entries(source.artifacts)) {
|
|
1539
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return { error: `artifact ${kind} \u5FC5\u987B\u662F\u5BF9\u8C61` };
|
|
1540
|
+
const rule = raw;
|
|
1541
|
+
for (const key of Object.keys(rule)) {
|
|
1542
|
+
if (key !== "phases" && key !== "roles" && key !== "paths") return { error: `artifact ${kind} \u672A\u77E5\u5B57\u6BB5\uFF1A${key}` };
|
|
1543
|
+
}
|
|
1544
|
+
if (!Array.isArray(rule.phases) || rule.phases.length === 0 || !rule.phases.every((phase) => phase === "*" || typeof phase === "string" && PHASES2.has(phase))) {
|
|
1545
|
+
return { error: `artifact ${kind} phases \u975E\u6CD5` };
|
|
1546
|
+
}
|
|
1547
|
+
if (!Array.isArray(rule.roles) || rule.roles.length === 0 || !rule.roles.every((role) => role === "root" || role === "worker")) {
|
|
1548
|
+
return { error: `artifact ${kind} roles \u975E\u6CD5` };
|
|
1549
|
+
}
|
|
1550
|
+
if (!Array.isArray(rule.paths) || rule.paths.length === 0) return { error: `artifact ${kind} paths \u975E\u6CD5` };
|
|
1551
|
+
const paths = [];
|
|
1552
|
+
for (const item of rule.paths) {
|
|
1553
|
+
if (typeof item !== "string" || item.trim() === "") return { error: `artifact ${kind} path \u975E\u6CD5` };
|
|
1554
|
+
const normalized = normalizeArtifactPath(item);
|
|
1555
|
+
if (path9.isAbsolute(normalized) || normalized.split("/").includes("..")) return { error: `artifact ${kind} path \u8D8A\u754C\uFF1A${item}` };
|
|
1556
|
+
if (isReservedPath(normalized)) return { error: `artifact ${kind} path \u662F\u4FDD\u7559\u8DEF\u5F84\uFF1A${item}` };
|
|
1557
|
+
paths.push(normalized);
|
|
1558
|
+
}
|
|
1559
|
+
artifacts[kind] = { phases: [...rule.phases], roles: [...rule.roles], paths };
|
|
1560
|
+
}
|
|
1561
|
+
return { policy: { version: 1, artifacts } };
|
|
1562
|
+
}
|
|
1563
|
+
function loadArtifactPolicy(root) {
|
|
1564
|
+
const file = path9.join(root, ARTIFACT_POLICY_PATH);
|
|
1565
|
+
if (!existsSync4(file)) return { error: "artifact policy \u4E0D\u5B58\u5728\uFF1B\u5148\u8FD0\u884C /gate-init \u5E76\u586B\u5199 .agents/gate/artifact-policy.json" };
|
|
1566
|
+
if (lstatSync4(file).isSymbolicLink()) return { error: "artifact policy \u4E0D\u80FD\u662F symlink" };
|
|
1567
|
+
try {
|
|
1568
|
+
return parseArtifactPolicy(JSON.parse(readFileSync3(file, "utf8")));
|
|
1569
|
+
} catch {
|
|
1570
|
+
return { error: "artifact policy \u4E0D\u662F\u6709\u6548 JSON" };
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
function segmentMatches(pattern, value) {
|
|
1574
|
+
const regex = new RegExp(`^${pattern.split("").map((char) => {
|
|
1575
|
+
if (char === "*") return "[^/]*";
|
|
1576
|
+
if (char === "?") return "[^/]";
|
|
1577
|
+
return char.replace(/[\\^$.*+?()[\]{}|]/gu, "\\$&");
|
|
1578
|
+
}).join("")}$`, "u");
|
|
1579
|
+
return regex.test(value);
|
|
1580
|
+
}
|
|
1581
|
+
function pathMatches(pattern, relative) {
|
|
1582
|
+
const patternParts = pattern.split("/").filter(Boolean);
|
|
1583
|
+
const valueParts = relative.split("/").filter(Boolean);
|
|
1584
|
+
const walk = (p, v) => {
|
|
1585
|
+
if (p === patternParts.length) return v === valueParts.length;
|
|
1586
|
+
const part = patternParts[p] ?? "";
|
|
1587
|
+
if (part === "**") return walk(p + 1, v) || v < valueParts.length && walk(p, v + 1);
|
|
1588
|
+
if (v >= valueParts.length) return false;
|
|
1589
|
+
if (!segmentMatches(part, valueParts[v] ?? "")) return false;
|
|
1590
|
+
return walk(p + 1, v + 1);
|
|
1591
|
+
};
|
|
1592
|
+
return walk(0, 0);
|
|
1593
|
+
}
|
|
1594
|
+
function artifactRuleAllows(policy, state, worker, kind) {
|
|
1595
|
+
if (state.phase === "blocked") return { error: "blocked \u4E0D\u5141\u8BB8 artifact \u5199\u5165" };
|
|
1596
|
+
const rule = policy.artifacts[kind];
|
|
1597
|
+
if (rule === void 0) return { error: `artifact kind \u672A\u6388\u6743\uFF1A${kind}` };
|
|
1598
|
+
const phaseKey = methodologyKeyFor(state);
|
|
1599
|
+
if (!rule.phases.includes("*") && !rule.phases.includes(phaseKey)) return { error: `\u5F53\u524D\u9636\u6BB5 ${phaseKey} \u4E0D\u5141\u8BB8 artifact ${kind}` };
|
|
1600
|
+
if (!rule.roles.includes(worker ? "worker" : "root")) return { error: `\u5F53\u524D\u89D2\u8272\u4E0D\u5141\u8BB8 artifact ${kind}` };
|
|
1601
|
+
return { rule };
|
|
1602
|
+
}
|
|
1603
|
+
function checkArtifactPath(root, rule, requested, kind) {
|
|
1604
|
+
const normalized = normalizeArtifactPath(requested);
|
|
1605
|
+
if (path9.isAbsolute(normalized) || normalized.split("/").includes("..")) return { error: "artifact path \u8D8A\u754C" };
|
|
1606
|
+
if (isReservedPath(normalized)) return { error: "artifact path \u662F\u4FDD\u7559\u8DEF\u5F84" };
|
|
1607
|
+
if (!rule.paths.some((pattern) => pathMatches(pattern, normalized))) return { error: `artifact ${kind} path \u4E0D\u5728\u767D\u540D\u5355\uFF1A${requested}` };
|
|
1608
|
+
const absolute = path9.resolve(root, normalized);
|
|
1609
|
+
if (!isInside(root, absolute)) return { error: "artifact path \u8D8A\u754C" };
|
|
1610
|
+
if (pathHasSymlink(root, absolute)) return { error: "artifact path \u5305\u542B symlink" };
|
|
1611
|
+
return { absolute, relative: path9.relative(root, absolute).replaceAll("\\", "/") };
|
|
1612
|
+
}
|
|
1613
|
+
function writeArtifact(root, requested, content) {
|
|
1614
|
+
const normalized = normalizeArtifactPath(requested);
|
|
1615
|
+
if (path9.isAbsolute(normalized) || normalized.split("/").includes("..") || isReservedPath(normalized)) return { error: "artifact path \u88AB\u62D2\u7EDD" };
|
|
1616
|
+
const target = path9.resolve(root, normalized);
|
|
1617
|
+
if (!isInside(root, target)) return { error: "artifact path \u8D8A\u754C" };
|
|
1618
|
+
if (pathHasSymlink(root, target)) return { error: "artifact path \u5305\u542B symlink" };
|
|
1619
|
+
mkdirSync4(path9.dirname(target), { recursive: true });
|
|
1620
|
+
writeFileSync4(target, content);
|
|
1621
|
+
return { relative: path9.relative(root, target).replaceAll("\\", "/") };
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1489
1624
|
// src/commands.ts
|
|
1490
1625
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
1626
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
1491
1627
|
|
|
1492
1628
|
// src/command-catalog.ts
|
|
1493
1629
|
var GATE_COMMANDS = [
|
|
1494
1630
|
{ name: "gate-init", description: "\u521D\u59CB\u5316 notes\u3001AGENTS.md\u3001verify-notes\u3001CI\u3001git \u4E0E\u57FA\u7840 .gitignore", hint: null, when: "any" },
|
|
1495
|
-
{ name: "gate-enter-plan", description: "\u57FA\u4E8E\u8BA8\u8BBA\u5171\u8BC6\u3001\u65E2\u6709\u51B3\u7B56\u4E0E\u7528\u6237\
|
|
1631
|
+
{ name: "gate-enter-plan", description: "\u57FA\u4E8E\u8BA8\u8BBA\u5171\u8BC6\u3001\u65E2\u6709\u51B3\u7B56\u4E0E\u7528\u6237\u9700\u6C42\u8FDB\u5165 planning", hint: "<\u5177\u4F53\u5DE5\u4F5C\u4E8B\u9879\u6216\u4E0A\u4E0B\u6587\u8BED\u4E49>", when: "discuss/complete" },
|
|
1496
1632
|
{ name: "gate-enter-implement", description: "\u57FA\u4E8E\u5DF2\u786E\u8BA4\u8BA1\u5212\u6216\u8BA8\u8BBA\u540E\u7684\u5B9E\u65BD\u8BF4\u660E\u8FDB\u5165 implementation", hint: "<\u5177\u4F53\u5DE5\u4F5C\u4E8B\u9879\u6216\u4E0A\u4E0B\u6587\u8BED\u4E49>", when: "discuss/complete" },
|
|
1633
|
+
{ name: "gate-prd", description: "\u5728 discuss \u4E2D\u6309\u7528\u6237\u8981\u6C42\u521B\u5EFA\u6216\u66F4\u65B0 PRD", hint: "[\u4E3B\u9898\u6216\u4E0A\u4E0B\u6587\u8BED\u4E49]", when: "discuss" },
|
|
1497
1634
|
{ name: "gate-fix-bug", description: "\u4E3A\u5DF2\u6388\u6743\u7F3A\u9677\u6D3E\u53D1 fix worker\uFF0C\u57FA\u4E8E\u63CF\u8FF0\u6216\u5F53\u524D\u4E0A\u4E0B\u6587\u4FEE\u590D", hint: "<\u5177\u4F53\u5DE5\u4F5C\u4E8B\u9879\u6216\u4E0A\u4E0B\u6587\u8BED\u4E49>", when: "discuss/complete" },
|
|
1498
|
-
{ name: "gate-docs", description: "\u8FDB\u5165\u6587\u6863\u6CBB\u7406\uFF0C\u6309\u9879\u76EE\u6587\u6863\u89C4\u8303\u5904\u7406", hint:
|
|
1635
|
+
{ name: "gate-docs", description: "\u8FDB\u5165\u6587\u6863\u6CBB\u7406\uFF0C\u6309\u9879\u76EE\u6587\u6863\u89C4\u8303\u5904\u7406", hint: "[\u6587\u6863\u8303\u56F4\u6216\u4E0A\u4E0B\u6587\u8BED\u4E49]", when: "discuss/complete" },
|
|
1499
1636
|
{ name: "gate-implement-review", description: "\u5BF9\u5F53\u524D\u6539\u52A8\u505A\u72EC\u7ACB\u53EA\u8BFB code review", hint: "<\u5177\u4F53\u5DE5\u4F5C\u4E8B\u9879\u6216\u4E0A\u4E0B\u6587\u8BED\u4E49>", when: "discuss/complete" },
|
|
1500
1637
|
{ name: "gate-code-verify", description: "\u5BF9\u5F53\u524D\u6539\u52A8\u505A\u72EC\u7ACB\u53EA\u8BFB\u9A8C\u6536\uFF08gate-user-verify\uFF09", hint: "<\u5177\u4F53\u5DE5\u4F5C\u4E8B\u9879\u6216\u4E0A\u4E0B\u6587\u8BED\u4E49>", when: "discuss/complete" },
|
|
1501
1638
|
{ name: "gate-propose-review", description: "\u5BF9\u63D0\u6848\u505A\u72EC\u7ACB\u53EA\u8BFB\u4E09\u8F74\u8BC4\u5BA1", hint: "<\u5177\u4F53\u5DE5\u4F5C\u4E8B\u9879\u6216\u4E0A\u4E0B\u6587\u8BED\u4E49>", when: "discuss/complete" },
|
|
@@ -1554,22 +1691,25 @@ function registerCommands(ctx, deps) {
|
|
|
1554
1691
|
command2("gate-init", async (invocation) => {
|
|
1555
1692
|
const workspace = workspaceBoundary(ctx, invocation.agent);
|
|
1556
1693
|
if (workspace === void 0) return { kind: "error", text: "\u65E0\u6CD5\u786E\u5B9A\u5F53\u524D\u4F1A\u8BDD\u5DE5\u4F5C\u533A" };
|
|
1557
|
-
const root =
|
|
1694
|
+
const root = path10.resolve(workspace);
|
|
1558
1695
|
let provisioned;
|
|
1559
1696
|
let projectScripts;
|
|
1560
1697
|
let projectCi;
|
|
1561
1698
|
let projectGit;
|
|
1699
|
+
let artifactPolicy;
|
|
1562
1700
|
try {
|
|
1563
1701
|
provisioned = provisionNotes(root);
|
|
1702
|
+
artifactPolicy = provisionArtifactPolicy(root);
|
|
1564
1703
|
projectCi = provisionCi(root);
|
|
1565
1704
|
projectScripts = provisionProjectScripts(root);
|
|
1566
1705
|
projectGit = provisionGit(root);
|
|
1567
1706
|
} catch (error) {
|
|
1568
1707
|
return { kind: "error", text: `notes \u521D\u59CB\u5316\u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}` };
|
|
1569
1708
|
}
|
|
1570
|
-
wake(invocation, store.read(root, invocation.agent), `Gate \u5DF2\u521D\u59CB\u5316\uFF1A${root}\uFF08phase=discuss\uFF09\uFF0Cnotes \u5DF2\u521D\u59CB\u5316\u4E3A ${provisioned.notes}\uFF0CAGENTS.md \u5DF2\u66F4\u65B0\uFF0Cverify-notes \u5DF2\u914D\u7F6E\u4E3A ${projectScripts.command}\uFF0CCI \u5DF2\u914D\u7F6E\u4E3A ${projectCi.workflow}\uFF0Cgit\uFF1A${projectGit.git}${projectGit.gitError === null ? "" : `\uFF08${projectGit.gitError}\uFF09`}\uFF0Cignore\uFF1A${projectGit.ignore}\u3002`, "gate-init");
|
|
1709
|
+
wake(invocation, store.read(root, invocation.agent), `Gate \u5DF2\u521D\u59CB\u5316\uFF1A${root}\uFF08phase=discuss\uFF09\uFF0Cnotes \u5DF2\u521D\u59CB\u5316\u4E3A ${provisioned.notes}\uFF0Cartifact policy \u4E3A ${artifactPolicy.path}\uFF08${artifactPolicy.status}\uFF09\uFF0CAGENTS.md \u5DF2\u66F4\u65B0\uFF0Cverify-notes \u5DF2\u914D\u7F6E\u4E3A ${projectScripts.command}\uFF0CCI \u5DF2\u914D\u7F6E\u4E3A ${projectCi.workflow}\uFF0Cgit\uFF1A${projectGit.git}${projectGit.gitError === null ? "" : `\uFF08${projectGit.gitError}\uFF09`}\uFF0Cignore\uFF1A${projectGit.ignore}\u3002`, "gate-init");
|
|
1571
1710
|
return { kind: "success", text: `Gate \u5DF2\u521D\u59CB\u5316\uFF1A${root}\uFF08phase=discuss\uFF09
|
|
1572
1711
|
notes\uFF1A${provisioned.notes}
|
|
1712
|
+
artifact policy\uFF1A${artifactPolicy.path}\uFF08${artifactPolicy.status}\uFF09
|
|
1573
1713
|
AGENTS\uFF1A${provisioned.agents}
|
|
1574
1714
|
verify-notes\uFF1A${projectScripts.command}
|
|
1575
1715
|
CI\uFF1A${projectCi.workflow}
|
|
@@ -1606,6 +1746,19 @@ ignore\uFF1A${projectGit.ignore}` };
|
|
|
1606
1746
|
wake(invocation, result.state, `Gate \u5DF2\u8FDB\u5165 implement\uFF1A${result.state.task ?? ""}${direct ? "\uFF08\u76F4\u63A5\u5B9E\u65BD\uFF0C\u672A\u8D70 planning\uFF09" : ""}\u3002\u7528\u6237\u8BF4\u660E\uFF1A${constraints}\u3002`, "gate-enter-implement");
|
|
1607
1747
|
return { kind: "success", text: `\u8FDB\u5165 implement\uFF1A${result.state.task}${direct ? "\uFF08\u76F4\u63A5\u5B9E\u65BD\uFF09" : ""}` };
|
|
1608
1748
|
});
|
|
1749
|
+
command2("gate-prd", async (invocation) => {
|
|
1750
|
+
const gate = resolveGate(ctx, invocation.agent);
|
|
1751
|
+
const state = store.read(gate.root, invocation.agent);
|
|
1752
|
+
if (state.phase !== "discuss") return { kind: "error", text: "gate-prd \u53EA\u5728 discuss \u9636\u6BB5\u53EF\u7528" };
|
|
1753
|
+
const topic = String(invocation.rawInput ?? "").trim() || "\u5F53\u524D\u8BA8\u8BBA";
|
|
1754
|
+
let skill = "";
|
|
1755
|
+
try {
|
|
1756
|
+
skill = readFileSync4(path10.join(SKILLS, "gate-prd", "SKILL.md"), "utf8");
|
|
1757
|
+
} catch {
|
|
1758
|
+
}
|
|
1759
|
+
wake(invocation, state, ["\u8BF7\u6309 gate-prd \u6280\u80FD\u521B\u5EFA\u6216\u66F4\u65B0 PRD\u3002", "\u4E3B\u9898/\u4E0A\u4E0B\u6587\uFF1A" + topic, skill].filter(Boolean).join("\n\n"), "gate-prd");
|
|
1760
|
+
return { kind: "success", text: "\u5DF2\u8BF7\u6C42 PRD\uFF1A" + topic };
|
|
1761
|
+
});
|
|
1609
1762
|
command2("gate-fix-bug", async (invocation) => {
|
|
1610
1763
|
const brief = String(invocation.rawInput ?? "").trim();
|
|
1611
1764
|
const gate = resolveGate(ctx, invocation.agent);
|
|
@@ -1624,19 +1777,20 @@ ignore\uFF1A${projectGit.ignore}` };
|
|
|
1624
1777
|
return { kind: "success", text: `\u8FDB\u5165 fix-bug\uFF1A${scope2}` };
|
|
1625
1778
|
});
|
|
1626
1779
|
command2("gate-docs", async (invocation) => {
|
|
1780
|
+
const scope2 = String(invocation.rawInput ?? "").trim() || "\u6587\u6863\u6CBB\u7406";
|
|
1627
1781
|
const gate = resolveGate(ctx, invocation.agent);
|
|
1628
1782
|
const state = store.read(gate.root, invocation.agent);
|
|
1629
1783
|
if (state.phase !== "discuss" && state.phase !== "complete") return { kind: "error", text: `\u5F53\u524D phase \u662F ${state.phase}` };
|
|
1630
1784
|
if (state.work === "blocked" || state.blockReason !== null) return { kind: "error", text: `\u5F53\u524D\u88AB blocked\uFF1A${state.blockReason ?? "unknown"}\uFF1B\u5148 /gate-resume` };
|
|
1631
1785
|
state.phase = "docs";
|
|
1632
|
-
state.task =
|
|
1786
|
+
state.task = scope2;
|
|
1633
1787
|
state.work = "active";
|
|
1634
1788
|
state.blockReason = null;
|
|
1635
1789
|
state.blockedFrom = null;
|
|
1636
1790
|
state.node = null;
|
|
1637
1791
|
store.write(gate.root, invocation.agent, state);
|
|
1638
|
-
wake(invocation, state,
|
|
1639
|
-
return { kind: "success", text:
|
|
1792
|
+
wake(invocation, state, `Gate \u5DF2\u8FDB\u5165 docs\uFF1A${scope2}\u3002`, "gate-docs");
|
|
1793
|
+
return { kind: "success", text: `\u8FDB\u5165 docs\uFF1A${scope2}` };
|
|
1640
1794
|
});
|
|
1641
1795
|
command2("gate-implement-review", async (invocation) => {
|
|
1642
1796
|
const brief = String(invocation.rawInput ?? "").trim();
|
|
@@ -1766,8 +1920,8 @@ ${JSON.stringify(check.results, null, 2)}` };
|
|
|
1766
1920
|
const node = resolveNodeBinary();
|
|
1767
1921
|
if (node === void 0) return { kind: "error", text: "notes-runtime-unavailable" };
|
|
1768
1922
|
try {
|
|
1769
|
-
const script =
|
|
1770
|
-
const output = execFileSync3(node, [script, "--root",
|
|
1923
|
+
const script = path10.join(NOTES_SKILL_DIR, "scripts", "archive-agent-note.ts");
|
|
1924
|
+
const output = execFileSync3(node, [script, "--root", path10.join(gate.root, ".agents", "notes"), ...args], { cwd: gate.root, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
1771
1925
|
return { kind: "success", text: String(output).trim() || "Note \u5DF2\u5F52\u6863" };
|
|
1772
1926
|
} catch (error) {
|
|
1773
1927
|
return { kind: "error", text: String(error?.stderr ?? error?.message ?? error).trim() };
|
|
@@ -1778,11 +1932,11 @@ ${JSON.stringify(check.results, null, 2)}` };
|
|
|
1778
1932
|
const raw = String(invocation.rawInput ?? "").trim();
|
|
1779
1933
|
const args = raw === "" ? [] : raw.split(/\s+/u);
|
|
1780
1934
|
const target = args[0] ?? "board.html";
|
|
1781
|
-
const title = args[1] ?? `${
|
|
1935
|
+
const title = args[1] ?? `${path10.basename(gate.root)} \u51B3\u7B56\u770B\u677F`;
|
|
1782
1936
|
const node = resolveNodeBinary();
|
|
1783
1937
|
if (node === void 0) return { kind: "error", text: "notes-runtime-unavailable" };
|
|
1784
1938
|
try {
|
|
1785
|
-
const script =
|
|
1939
|
+
const script = path10.join(NOTES_SKILL_DIR, "scripts", "build-board.ts");
|
|
1786
1940
|
const output = execFileSync3(node, [script, "--init", target, title], { cwd: gate.root, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
1787
1941
|
return { kind: "success", text: String(output).trim() || `\u51B3\u7B56\u770B\u677F\u5DF2\u751F\u6210\uFF1A${target}` };
|
|
1788
1942
|
} catch (error) {
|
|
@@ -1794,8 +1948,8 @@ ${JSON.stringify(check.results, null, 2)}` };
|
|
|
1794
1948
|
const node = resolveNodeBinary();
|
|
1795
1949
|
if (node === void 0) return { kind: "error", text: "notes-runtime-unavailable" };
|
|
1796
1950
|
try {
|
|
1797
|
-
const script =
|
|
1798
|
-
const output = execFileSync3(node, [script, "--root",
|
|
1951
|
+
const script = path10.join(NOTES_SKILL_DIR, "scripts", "verify-archived-agent-notes.ts");
|
|
1952
|
+
const output = execFileSync3(node, [script, "--root", path10.join(gate.root, ".agents", "notes"), "--write"], { cwd: gate.root, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
1799
1953
|
return { kind: "success", text: String(output).trim() || "archived manifest \u5DF2\u5199\u5165" };
|
|
1800
1954
|
} catch (error) {
|
|
1801
1955
|
return { kind: "error", text: String(error?.stderr ?? error?.message ?? error).trim() };
|
|
@@ -1864,7 +2018,7 @@ function registerStartPhase(ctx, deps) {
|
|
|
1864
2018
|
|
|
1865
2019
|
// src/tools/record.ts
|
|
1866
2020
|
import { createHash } from "node:crypto";
|
|
1867
|
-
import { existsSync as
|
|
2021
|
+
import { existsSync as existsSync5, lstatSync as lstatSync5, readFileSync as readFileSync5 } from "node:fs";
|
|
1868
2022
|
function registerRecord(ctx, deps) {
|
|
1869
2023
|
const { store, reviewLimit, wake } = deps;
|
|
1870
2024
|
ctx.tools.register({
|
|
@@ -2070,11 +2224,11 @@ function registerRecord(ctx, deps) {
|
|
|
2070
2224
|
for (const item of evidence) {
|
|
2071
2225
|
const target = classifyPath(gate.workspace, item);
|
|
2072
2226
|
try {
|
|
2073
|
-
if (target.outside || !
|
|
2227
|
+
if (target.outside || !existsSync5(target.absolute) || !lstatSync5(target.absolute).isFile()) {
|
|
2074
2228
|
invalidEvidence.push(item);
|
|
2075
2229
|
continue;
|
|
2076
2230
|
}
|
|
2077
|
-
const bytes =
|
|
2231
|
+
const bytes = readFileSync5(target.absolute);
|
|
2078
2232
|
records.push({ path: target.relative, size: bytes.length, sha256: createHash("sha256").update(bytes).digest("hex") });
|
|
2079
2233
|
} catch {
|
|
2080
2234
|
invalidEvidence.push(item);
|
|
@@ -2193,8 +2347,8 @@ function registerStatus(ctx, deps) {
|
|
|
2193
2347
|
}
|
|
2194
2348
|
|
|
2195
2349
|
// src/tools/notes.ts
|
|
2196
|
-
import { existsSync as
|
|
2197
|
-
import
|
|
2350
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "node:fs";
|
|
2351
|
+
import path11 from "node:path";
|
|
2198
2352
|
function registerNoteTools(ctx, deps) {
|
|
2199
2353
|
const { store, reviewLimit, wake } = deps;
|
|
2200
2354
|
ctx.tools.register({
|
|
@@ -2220,7 +2374,7 @@ function registerNoteTools(ctx, deps) {
|
|
|
2220
2374
|
return { ok: false, code: "planning-only", reason: "gate_note_write \u53EA\u5141\u8BB8 planning worker \u5728 plan/plan-revise \u8282\u70B9\u4F7F\u7528\u3002" };
|
|
2221
2375
|
}
|
|
2222
2376
|
let relative = String(args.path ?? "").replaceAll("\\", "/").replace(/^\.?\//u, "");
|
|
2223
|
-
if (relative === "" || relative.includes("..") ||
|
|
2377
|
+
if (relative === "" || relative.includes("..") || path11.isAbsolute(relative) || !relative.endsWith(".md")) {
|
|
2224
2378
|
return { ok: false, code: "invalid-path" };
|
|
2225
2379
|
}
|
|
2226
2380
|
let lifecycle = "proposed";
|
|
@@ -2232,16 +2386,16 @@ function registerNoteTools(ctx, deps) {
|
|
|
2232
2386
|
if (!/^(?:feature|bug-fix|simplification|architecture|process|testing)\/\d{4}-\d{2}-\d{2}-.+\.md$/u.test(relative)) {
|
|
2233
2387
|
return { ok: false, code: "invalid-path", reason: "\u9700\u8981 <class>/yyyy-mm-dd-topic.md\uFF0C\u6216 proposed|implemented/<class>/...\u3002" };
|
|
2234
2388
|
}
|
|
2235
|
-
const base =
|
|
2236
|
-
const absolute =
|
|
2389
|
+
const base = path11.join(gate.root, ".agents", "notes", String(lifecycle));
|
|
2390
|
+
const absolute = path11.resolve(base, relative);
|
|
2237
2391
|
if (!isInside(base, absolute)) return { ok: false, code: "invalid-path" };
|
|
2238
2392
|
if (pathHasSymlink(base, absolute)) return { ok: false, code: "invalid-path" };
|
|
2239
|
-
if (lifecycle === "implemented" && !
|
|
2393
|
+
if (lifecycle === "implemented" && !existsSync6(absolute)) {
|
|
2240
2394
|
return { ok: false, code: "implemented-must-exist", reason: "implemented Note \u53EA\u80FD\u539F\u5730\u7F16\u8F91\uFF0C\u4E0D\u80FD\u65B0\u5EFA\u3002" };
|
|
2241
2395
|
}
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
return { ok: true, path:
|
|
2396
|
+
mkdirSync5(path11.dirname(absolute), { recursive: true });
|
|
2397
|
+
writeFileSync5(absolute, String(args.content ?? ""));
|
|
2398
|
+
return { ok: true, path: path11.relative(gate.root, absolute).replaceAll("\\", "/"), lifecycle };
|
|
2245
2399
|
}
|
|
2246
2400
|
});
|
|
2247
2401
|
ctx.tools.register({
|
|
@@ -2264,7 +2418,7 @@ function registerNoteTools(ctx, deps) {
|
|
|
2264
2418
|
if (!check.ok) return { ...check, state: normalizeState(state) };
|
|
2265
2419
|
if (state.node === "note-lifecycle") {
|
|
2266
2420
|
if (state.noteFinalized !== true) return { ok: false, code: "note-lifecycle-incomplete", reason: "\u5148\u8C03\u7528 gate_note_finalize\u3002", state: normalizeState(state) };
|
|
2267
|
-
const implemented = state.notePaths.length > 0 && state.notePaths.every((entry) => entry.lifecycle === "implemented" &&
|
|
2421
|
+
const implemented = state.notePaths.length > 0 && state.notePaths.every((entry) => entry.lifecycle === "implemented" && existsSync6(path11.join(gate.root, entry.path)));
|
|
2268
2422
|
if (!implemented) return { ok: false, code: "note-lifecycle-incomplete", reason: "notePaths \u5C1A\u672A\u5168\u90E8\u8FDB\u5165 implemented\u3002", results: check.results, state: normalizeState(state) };
|
|
2269
2423
|
state.noteVerified = true;
|
|
2270
2424
|
store.write(gate.root, exec.agent, state);
|
|
@@ -2312,9 +2466,9 @@ function registerNoteTools(ctx, deps) {
|
|
|
2312
2466
|
const recovered = recoverFinalize(gate.root, sessionId);
|
|
2313
2467
|
for (const entry of state.notePaths) {
|
|
2314
2468
|
if (entry.lifecycle !== "proposed") continue;
|
|
2315
|
-
const source =
|
|
2469
|
+
const source = path11.join(gate.root, entry.path);
|
|
2316
2470
|
const targetRelative = entry.path.replaceAll("\\", "/").replace(".agents/notes/proposed/", ".agents/notes/implemented/");
|
|
2317
|
-
if (!
|
|
2471
|
+
if (!existsSync6(source) && existsSync6(path11.join(gate.root, targetRelative))) {
|
|
2318
2472
|
entry.path = targetRelative;
|
|
2319
2473
|
entry.lifecycle = "implemented";
|
|
2320
2474
|
recovered.push("adopted-target");
|
|
@@ -2355,7 +2509,9 @@ var GATE_TOOLS = [
|
|
|
2355
2509
|
{ name: "gate_note_write", summary: "planning worker \u5199 proposed Note \u6216\u539F\u5730\u66F4\u65B0 implemented Note\u3002" },
|
|
2356
2510
|
{ name: "gate_note_verify", summary: "\u5728 note-lifecycle \u4E2D\u8FD0\u884C notes \u6821\u9A8C\uFF1Broot-only\u3002" },
|
|
2357
2511
|
{ name: "gate_note_finalize", summary: "\u628A proposed Note \u79FB\u5230 implemented \u5E76\u66F4\u65B0\u5934\u5757\uFF1Broot-only\u3002" },
|
|
2358
|
-
{ name: "gate_help", summary: "\u5217\u51FA gate \u547D\u4EE4\u3001\u5DE5\u5177\u548C\u5F53\u524D phase/node\uFF1B\u53EA\u8BFB\uFF0Croot-only\u3002" }
|
|
2512
|
+
{ name: "gate_help", summary: "\u5217\u51FA gate \u547D\u4EE4\u3001\u5DE5\u5177\u548C\u5F53\u524D phase/node\uFF1B\u53EA\u8BFB\uFF0Croot-only\u3002" },
|
|
2513
|
+
{ name: "gate_artifact_write", summary: "\u6309 artifact policy \u5199\u767D\u540D\u5355\u6587\u4EF6\uFF1B\u4E0D\u8D70\u666E\u901A workspace write\u3002" },
|
|
2514
|
+
{ name: "gate_research_dispatch", summary: "root-only \u5728 discuss/plan \u6D3E research worker\uFF1Bworker \u53EA\u5199 research artifact\u3002" }
|
|
2359
2515
|
];
|
|
2360
2516
|
|
|
2361
2517
|
// src/tools/help.ts
|
|
@@ -2407,6 +2563,122 @@ function registerHelp(ctx, deps) {
|
|
|
2407
2563
|
});
|
|
2408
2564
|
}
|
|
2409
2565
|
|
|
2566
|
+
// src/tools/artifact.ts
|
|
2567
|
+
function registerArtifactTools(ctx, deps) {
|
|
2568
|
+
const { store } = deps;
|
|
2569
|
+
ctx.tools.register({
|
|
2570
|
+
name: "gate_artifact_write",
|
|
2571
|
+
description: "\u6309 .agents/gate/artifact-policy.json \u5199\u767D\u540D\u5355 artifact\uFF08handoff/prd/research \u7B49\uFF09\u3002\u53EA\u5199\u6587\u4EF6\uFF0C\u4E0D\u8D70\u666E\u901A workspace write \u6743\u9650\u3002",
|
|
2572
|
+
parameters: {
|
|
2573
|
+
type: "object",
|
|
2574
|
+
additionalProperties: false,
|
|
2575
|
+
properties: {
|
|
2576
|
+
kind: { type: "string", description: "policy \u4E2D\u7684 artifact kind\u3002" },
|
|
2577
|
+
path: { type: "string", description: "workspace \u5185\u76F8\u5BF9\u8DEF\u5F84\u3002" },
|
|
2578
|
+
content: { type: "string" }
|
|
2579
|
+
},
|
|
2580
|
+
required: ["kind", "path", "content"]
|
|
2581
|
+
},
|
|
2582
|
+
output: {
|
|
2583
|
+
schema: { type: "object", additionalProperties: true },
|
|
2584
|
+
render: (_args, value) => [{ type: "text", text: typeof value?.text === "string" ? value.text : JSON.stringify(value) }]
|
|
2585
|
+
},
|
|
2586
|
+
async execute(args, exec) {
|
|
2587
|
+
const gate = resolveGate(ctx, exec.agent);
|
|
2588
|
+
if (gate.root === void 0) return { ok: false, code: "state-unavailable", reason: "\u65E0\u6CD5\u786E\u5B9A\u5DE5\u4F5C\u533A\u3002" };
|
|
2589
|
+
const effective = effectiveState(ctx, exec.agent, gate, store);
|
|
2590
|
+
const loaded = loadArtifactPolicy(gate.root);
|
|
2591
|
+
if (loaded.policy === void 0) return { ok: false, code: "artifact-policy-invalid", reason: loaded.error };
|
|
2592
|
+
const allowed = artifactRuleAllows(loaded.policy, effective.state, effective.worker, String(args?.kind ?? ""));
|
|
2593
|
+
if (allowed.rule === void 0) return { ok: false, code: "artifact-denied", reason: allowed.error };
|
|
2594
|
+
const checked = checkArtifactPath(gate.root, allowed.rule, String(args?.path ?? ""), String(args?.kind ?? ""));
|
|
2595
|
+
if (checked.absolute === void 0) return { ok: false, code: "artifact-denied", reason: checked.error };
|
|
2596
|
+
const written = writeArtifact(gate.root, checked.relative ?? String(args?.path ?? ""), String(args?.content ?? ""));
|
|
2597
|
+
if (written.relative === void 0) return { ok: false, code: "artifact-write-failed", reason: written.error };
|
|
2598
|
+
return { ok: true, kind: String(args.kind), path: written.relative, bytes: Buffer.byteLength(String(args.content ?? "")) };
|
|
2599
|
+
}
|
|
2600
|
+
});
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2603
|
+
// src/tools/research.ts
|
|
2604
|
+
function registerResearchDispatch(ctx, deps) {
|
|
2605
|
+
const { store } = deps;
|
|
2606
|
+
ctx.tools.register({
|
|
2607
|
+
name: "gate_research_dispatch",
|
|
2608
|
+
description: "root-only\uFF1A\u5728 discuss \u6216 orchestrating/plan \u6D3E research worker\uFF0C\u7B49\u5F85\u7ED3\u679C\uFF1Bworker \u53EA\u80FD\u901A\u8FC7 artifact policy \u5199 research \u6587\u6863\u3002",
|
|
2609
|
+
parameters: {
|
|
2610
|
+
type: "object",
|
|
2611
|
+
additionalProperties: false,
|
|
2612
|
+
properties: {
|
|
2613
|
+
question: { type: "string" },
|
|
2614
|
+
purpose: { type: "string" }
|
|
2615
|
+
},
|
|
2616
|
+
required: ["question"]
|
|
2617
|
+
},
|
|
2618
|
+
output: {
|
|
2619
|
+
schema: { type: "object", additionalProperties: true },
|
|
2620
|
+
render: (_args, value) => [{ type: "text", text: typeof value?.text === "string" ? value.text : JSON.stringify(value) }]
|
|
2621
|
+
},
|
|
2622
|
+
async execute(args, exec) {
|
|
2623
|
+
const root = rootAgentOf(ctx, exec.agent);
|
|
2624
|
+
if (root?.id !== exec.agent?.id) return { ok: false, code: "root-only", reason: "gate_research_dispatch \u53EA\u80FD\u7531 root agent \u8C03\u7528\u3002" };
|
|
2625
|
+
const gate = resolveGate(ctx, exec.agent);
|
|
2626
|
+
if (gate.root === void 0) return { ok: false, code: "state-unavailable", reason: "\u65E0\u6CD5\u786E\u5B9A\u5DE5\u4F5C\u533A\u3002" };
|
|
2627
|
+
const state = store.read(gate.root, root);
|
|
2628
|
+
if (!(state.phase === "discuss" || state.phase === "orchestrating" && state.node === "plan")) {
|
|
2629
|
+
return { ok: false, code: "wrong-phase", reason: "research dispatch \u53EA\u5141\u8BB8 discuss \u6216 orchestrating/plan\u3002" };
|
|
2630
|
+
}
|
|
2631
|
+
const loaded = loadArtifactPolicy(gate.root);
|
|
2632
|
+
if (loaded.policy === void 0) return { ok: false, code: "artifact-policy-invalid", reason: loaded.error };
|
|
2633
|
+
const allowed = artifactRuleAllows(loaded.policy, state, true, "research");
|
|
2634
|
+
if (allowed.rule === void 0) return { ok: false, code: "artifact-denied", reason: allowed.error };
|
|
2635
|
+
const subagents = typeof ctx.get === "function" ? ctx.get("subagents") : void 0;
|
|
2636
|
+
if (subagents?.startActivation === void 0 || subagents?.getProvider === void 0) {
|
|
2637
|
+
return { ok: false, code: "subagents-unavailable", reason: "ctx.subagents \u4E0D\u53EF\u7528\uFF0C\u65E0\u6CD5\u6D3E research worker\u3002" };
|
|
2638
|
+
}
|
|
2639
|
+
const provider = subagents.getProvider("spawn");
|
|
2640
|
+
if (provider === void 0) return { ok: false, code: "provider-unavailable", reason: "ctx.subagents \u6CA1\u6709 spawn provider\u3002" };
|
|
2641
|
+
const question = String(args?.question ?? "").trim();
|
|
2642
|
+
if (question === "") return { ok: false, code: "question-required", reason: "research \u9700\u8981 question\u3002" };
|
|
2643
|
+
const purpose = String(args?.purpose ?? "").trim();
|
|
2644
|
+
const prompt = [{
|
|
2645
|
+
type: "text",
|
|
2646
|
+
text: [
|
|
2647
|
+
"Research worker task. Investigate primary sources and write a Markdown report.",
|
|
2648
|
+
`Question: ${question}`,
|
|
2649
|
+
purpose === "" ? "" : `Purpose: ${purpose}`,
|
|
2650
|
+
"Use gate_artifact_write({ kind: 'research', path, content }) to save the report.",
|
|
2651
|
+
"Prefer project docs research location; otherwise .scratch/research/. Cite sources. Do not modify product code."
|
|
2652
|
+
].filter(Boolean).join("\n")
|
|
2653
|
+
}];
|
|
2654
|
+
let activation;
|
|
2655
|
+
try {
|
|
2656
|
+
activation = await subagents.startActivation({
|
|
2657
|
+
provider: "spawn",
|
|
2658
|
+
label: "gate-research",
|
|
2659
|
+
prompt,
|
|
2660
|
+
parent: exec.agent,
|
|
2661
|
+
signal: exec.signal,
|
|
2662
|
+
delivery: "caller"
|
|
2663
|
+
});
|
|
2664
|
+
const result = await activation.result;
|
|
2665
|
+
const output = result?.output;
|
|
2666
|
+
const text = typeof output === "string" ? output : JSON.stringify(output ?? result);
|
|
2667
|
+
return { ok: true, name: activation.name, text };
|
|
2668
|
+
} catch (error) {
|
|
2669
|
+
return { ok: false, code: "research-dispatch-failed", reason: error instanceof Error ? error.message : String(error) };
|
|
2670
|
+
} finally {
|
|
2671
|
+
if (activation?.dispose !== void 0) {
|
|
2672
|
+
try {
|
|
2673
|
+
await activation.dispose();
|
|
2674
|
+
} catch {
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
});
|
|
2680
|
+
}
|
|
2681
|
+
|
|
2410
2682
|
// src/tools/index.ts
|
|
2411
2683
|
function registerTools(ctx, deps) {
|
|
2412
2684
|
registerStartPhase(ctx, deps);
|
|
@@ -2414,6 +2686,8 @@ function registerTools(ctx, deps) {
|
|
|
2414
2686
|
registerStatus(ctx, deps);
|
|
2415
2687
|
registerNoteTools(ctx, deps);
|
|
2416
2688
|
registerHelp(ctx, deps);
|
|
2689
|
+
registerArtifactTools(ctx, deps);
|
|
2690
|
+
registerResearchDispatch(ctx, deps);
|
|
2417
2691
|
}
|
|
2418
2692
|
|
|
2419
2693
|
// src/index.ts
|
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@ argument-hint: "What will the next session be used for?"
|
|
|
5
5
|
disable-model-invocation: true
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
Write a handoff document summarising the current conversation so a fresh agent can continue the work. Save
|
|
8
|
+
Write a handoff document summarising the current conversation so a fresh agent can continue the work. Save under `.scratch/handoff/` in the workspace. Use `gate_artifact_write({ kind: 'handoff', path: '.scratch/handoff/<name>.md', content })`; do not use generic `write`.
|
|
9
9
|
|
|
10
10
|
Include a "suggested skills" section in the document, which suggests skills that the agent should invoke.
|
|
11
11
|
|
|
@@ -6,6 +6,10 @@ argument-hint: "项目路径,或要覆盖的功能 / bug"
|
|
|
6
6
|
|
|
7
7
|
# Playwright E2E 配置
|
|
8
8
|
|
|
9
|
+
在 gate 中:模型启用本技能时走 implement;用户在 discuss 调用时只讨论方案,写 config/spec/AGENTS 与安装依赖在 implement 进行。
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
9
13
|
分工是固定的:**Agent 写 spec,Playwright runner 跑 spec。** 提需求的这一轮里,Agent 不打开浏览器一格格点页面——那是它最贵、最慢、最不可回归的用法;它只写 Node.js + Playwright 脚本,然后跑命令。脚本一旦落盘,「这个功能对不对」就从一轮轮截图变成一条几秒钟、几乎不花 token、随时可重跑的断言。
|
|
10
14
|
|
|
11
15
|
这个 skill 一次跑完,产出三样**落盘**的东西:可跑的 `playwright.config.ts`、项目里长期生效的测试规则、覆盖目标行为的 spec。之后日常由项目里的规则接管,换项目、换技术栈、或规则要改的时候再调它。
|
package/skills/gate-prd/SKILL.md
CHANGED
|
@@ -10,7 +10,7 @@ disable-model-invocation: true
|
|
|
10
10
|
|
|
11
11
|
## 位置与复用
|
|
12
12
|
|
|
13
|
-
遵循项目文档分层和既有 PRD
|
|
13
|
+
遵循项目文档分层和既有 PRD 格式。写入使用 `gate_artifact_write({ kind: 'prd', path, content })`,不要使用通用 `write`。优先更新同一产品主题已有的 PRD;没有已配置的存放位置时,先在对话中起草并请用户决定位置,不自行发布到外部系统。
|
|
14
14
|
|
|
15
15
|
## 内容
|
|
16
16
|
|
|
@@ -4,11 +4,11 @@ description: Investigate a question against high-trust primary sources and captu
|
|
|
4
4
|
user-invocable: false
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
调用 `gate_research_dispatch({ question, purpose })` 派 research worker,并等待结果。
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
worker 的职责:
|
|
10
10
|
|
|
11
11
|
1. Investigate the question against **primary sources** — official docs, source code, specs, first-party APIs — not a secondary write-up of them. Follow every claim back to the source that owns it.
|
|
12
12
|
2. Write the findings to a single Markdown file, citing each claim's source.
|
|
13
|
-
3. Save it
|
|
13
|
+
3. Save it with `gate_artifact_write({ kind: 'research', path, content })`;优先项目文档位置,否则 `.scratch/research/`。不要使用通用 `write`。
|
|
14
14
|
|
|
@@ -3,6 +3,8 @@ name: gate-writing-for-agents
|
|
|
3
3
|
description: Writing documents for agents. Use when creating or editing skills, or modifying AGENTS.md or CLAUDE.md.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
+
在 gate 中:修改 skill、AGENTS.md 或 agent 文档属于 implement 工作;discuss/plan 只讨论写法,不落盘。
|
|
7
|
+
|
|
6
8
|
Reference for writing any document an agent consumes — a skill, an `AGENTS.md`, a doc reached by a pointer. The packaging differs; the writing does not: the same levers make each one predictable — the agent taking the same _process_ every run, not producing the same output.
|
|
7
9
|
|
|
8
10
|
When the document you're writing is a skill, read [`SKILL-MECHANICS.md`](SKILL-MECHANICS.md) for frontmatter, invocation choice, and router skills.
|