dsh-gate-agent 0.1.4 → 0.1.6
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 +255 -4
- package/lib/index.js +512 -33
- 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-write-agent-notes/SKILL.md +3 -1
- package/skills/gate-write-agent-notes/references/verification.md +3 -1
- 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 修复已授权缺陷 |
|
|
@@ -152,6 +153,36 @@ Host 插件:用 phase/node 门禁组织讨论、规划、实施、评审、验
|
|
|
152
153
|
|
|
153
154
|
从 `discuss` 开始,不进入 planning/implement。
|
|
154
155
|
|
|
156
|
+
初始化会生成:
|
|
157
|
+
|
|
158
|
+
- `.agents/gate/verify-notes.mjs`
|
|
159
|
+
- `.agents/gate/ci/*.ts`
|
|
160
|
+
- `.agents/gate/artifact-policy.json`
|
|
161
|
+
- `.agents/gate/init-manifest.json`
|
|
162
|
+
|
|
163
|
+
`init-manifest.json` 记录生成物 hash、artifact-policy hash、AGENTS managed block、verify-notes script 和 notes 目录结构。它被 git 跟踪;同内容重跑 init 时字节稳定。
|
|
164
|
+
|
|
165
|
+
只读核对:
|
|
166
|
+
|
|
167
|
+
```text
|
|
168
|
+
gate_init_verify
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
它会检查:
|
|
172
|
+
|
|
173
|
+
- 5 个 CI 脚本 + launcher 的 SHA-256;
|
|
174
|
+
- `artifact-policy.json` 的 schema;用户修改后有效则显示 `customized`;
|
|
175
|
+
- AGENTS managed block;
|
|
176
|
+
- `scripts.verify-notes`;
|
|
177
|
+
- workflow 是否存在;
|
|
178
|
+
- notes 四目录结构。
|
|
179
|
+
|
|
180
|
+
不检查 Note 内容;Note 内容继续用:
|
|
181
|
+
|
|
182
|
+
```text
|
|
183
|
+
npm run verify-notes
|
|
184
|
+
```
|
|
185
|
+
|
|
155
186
|
### Planning
|
|
156
187
|
|
|
157
188
|
```text
|
|
@@ -245,7 +276,7 @@ review worker 使用 `gate-plan-review` 做 Standards/Spec/Architecture 三轴
|
|
|
245
276
|
/gate-docs
|
|
246
277
|
```
|
|
247
278
|
|
|
248
|
-
进入 `docs` phase
|
|
279
|
+
进入 `docs` phase,由主代理按项目文档规范处理。
|
|
249
280
|
|
|
250
281
|
### 暂停、恢复与讨论
|
|
251
282
|
|
|
@@ -306,6 +337,28 @@ gate_record({
|
|
|
306
337
|
gate_status()
|
|
307
338
|
```
|
|
308
339
|
|
|
340
|
+
### gate_artifact_write
|
|
341
|
+
|
|
342
|
+
按 `.agents/gate/artifact-policy.json` 写白名单 artifact;只写文件,不走普通 workspace write。支持精确文件、目录和 glob。
|
|
343
|
+
|
|
344
|
+
### gate_research_dispatch
|
|
345
|
+
|
|
346
|
+
root-only;在 discuss 或 `orchestrating/plan` 派 research worker。worker 只能通过 artifact policy 写 research 文档,不获得通用 workspace 写。
|
|
347
|
+
|
|
348
|
+
### gate_init_verify
|
|
349
|
+
|
|
350
|
+
root-only 只读校验 `/gate-init` 产物:
|
|
351
|
+
|
|
352
|
+
- `.agents/gate/init-manifest.json` 是否存在且有效;
|
|
353
|
+
- 5 个 CI 脚本 + `verify-notes.mjs` 的 hash;
|
|
354
|
+
- `artifact-policy.json` schema(custom 有效也通过);
|
|
355
|
+
- AGENTS managed block;
|
|
356
|
+
- `scripts.verify-notes`;
|
|
357
|
+
- workflow 存在性;
|
|
358
|
+
- notes 四目录。
|
|
359
|
+
|
|
360
|
+
不修改文件,不运行 `verify-notes`;Note 内容校验仍用 `npm run verify-notes`。
|
|
361
|
+
|
|
309
362
|
### Note 工具
|
|
310
363
|
|
|
311
364
|
- `gate_note_write`:planning worker 写 proposed Note,或原地更新 implemented Note;通用 `write`/`edit`/`apply_patch` 和仓库写被拒绝,只读 shell 可用。
|
|
@@ -334,7 +387,183 @@ gate_status()
|
|
|
334
387
|
|
|
335
388
|
## 配置
|
|
336
389
|
|
|
337
|
-
|
|
390
|
+
### artifact-policy.json
|
|
391
|
+
|
|
392
|
+
discuss 和 plan 默认不允许普通文件写。`handoff`、PRD、research 等产物通过**独立的 artifact 白名单**写入,而不是放开通用 `write`。
|
|
393
|
+
|
|
394
|
+
`/gate-init` 会创建:
|
|
395
|
+
|
|
396
|
+
```text
|
|
397
|
+
.agents/gate/artifact-policy.json
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
- 文件不存在时写入基础配置;
|
|
401
|
+
- 文件已存在时**不覆盖**,保留用户内容;
|
|
402
|
+
- 新建 `.gitignore` 时包含 `.scratch/`;已有 `.gitignore` 不重写。
|
|
403
|
+
|
|
404
|
+
#### 基础配置
|
|
405
|
+
|
|
406
|
+
```json
|
|
407
|
+
{
|
|
408
|
+
"version": 1,
|
|
409
|
+
"artifacts": {
|
|
410
|
+
"handoff": {
|
|
411
|
+
"phases": ["*"],
|
|
412
|
+
"roles": ["root"],
|
|
413
|
+
"paths": [".scratch/handoff/**"]
|
|
414
|
+
},
|
|
415
|
+
"prd": {
|
|
416
|
+
"phases": ["discuss"],
|
|
417
|
+
"roles": ["root"],
|
|
418
|
+
"paths": ["docs/prd/**", ".scratch/prd/**"]
|
|
419
|
+
},
|
|
420
|
+
"research": {
|
|
421
|
+
"phases": ["discuss", "orchestrating/plan"],
|
|
422
|
+
"roles": ["root", "worker"],
|
|
423
|
+
"paths": ["docs/research/**", ".scratch/research/**"]
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
#### 字段
|
|
430
|
+
|
|
431
|
+
| 字段 | 说明 |
|
|
432
|
+
|---|---|
|
|
433
|
+
| `version` | 必须为 `1` |
|
|
434
|
+
| `artifacts` | artifact kind 到规则的映射 |
|
|
435
|
+
| `kind` | 内置 `handoff`、`prd`、`research`;也可以增加自定义 kind |
|
|
436
|
+
| `phases` | 允许的 canonical phase/node key,或 `*` |
|
|
437
|
+
| `roles` | `root` 或 `worker` |
|
|
438
|
+
| `paths` | 相对 workspace 的文件、目录或 glob |
|
|
439
|
+
|
|
440
|
+
#### phases
|
|
441
|
+
|
|
442
|
+
使用 `gate_status` 返回的 `methodologyKey` 形式:
|
|
443
|
+
|
|
444
|
+
- `discuss`
|
|
445
|
+
- `docs`
|
|
446
|
+
- `complete`
|
|
447
|
+
- `orchestrating/plan`
|
|
448
|
+
- `orchestrating/plan-review`
|
|
449
|
+
- `orchestrating/plan-revise`
|
|
450
|
+
- `orchestrating/implement`
|
|
451
|
+
- `orchestrating/code-review`
|
|
452
|
+
- `orchestrating/implement-revise`
|
|
453
|
+
- `orchestrating/verify`
|
|
454
|
+
- `orchestrating/note-lifecycle`
|
|
455
|
+
- `orchestrating/fix-bug`
|
|
456
|
+
- `reviewing/code-review-standalone`
|
|
457
|
+
- `reviewing/proposal-review`
|
|
458
|
+
- `reviewing/verify-standalone`
|
|
459
|
+
|
|
460
|
+
`*` 表示任意**非 blocked** 状态。`blocked` 始终拒绝,即使规则写了 `*`。
|
|
461
|
+
|
|
462
|
+
#### roles
|
|
463
|
+
|
|
464
|
+
- `root`:主代理;
|
|
465
|
+
- `worker`:任何 gate worker,包括 planning worker 和 research worker。
|
|
466
|
+
|
|
467
|
+
#### paths
|
|
468
|
+
|
|
469
|
+
支持四种形式:
|
|
470
|
+
|
|
471
|
+
```json
|
|
472
|
+
"paths": [
|
|
473
|
+
"docs/prd/current.md",
|
|
474
|
+
"docs/prd/PRD-*.md",
|
|
475
|
+
".scratch/research/**/*.md",
|
|
476
|
+
".scratch/handoff/**"
|
|
477
|
+
]
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
| 形式 | 含义 |
|
|
481
|
+
|---|---|
|
|
482
|
+
| `docs/prd/current.md` | 只允许这个文件 |
|
|
483
|
+
| `docs/prd/*.md` | 只允许该目录下直接文件,不递归 |
|
|
484
|
+
| `.scratch/research/**/*.md` | 递归匹配 Markdown |
|
|
485
|
+
| `.scratch/handoff/**` | 允许该目录下任意层级文件 |
|
|
486
|
+
| `docs/prd/**` | 允许该目录下任意层级文件 |
|
|
487
|
+
|
|
488
|
+
规则:
|
|
489
|
+
|
|
490
|
+
- `*` 只匹配一个路径段;
|
|
491
|
+
- `**` 匹配零个或多个路径段;
|
|
492
|
+
- 普通路径没有通配符时按**明确文件**处理;
|
|
493
|
+
- 不允许绝对路径、`..`、workspace 外路径、symlink;
|
|
494
|
+
- 保留路径永远拒绝:
|
|
495
|
+
- `.agents/gate/artifact-policy.json`
|
|
496
|
+
- `.agents/notes/**`
|
|
497
|
+
- 只写文件;父目录可以由工具创建,但也要通过边界检查。
|
|
498
|
+
|
|
499
|
+
#### 如何按项目填写
|
|
500
|
+
|
|
501
|
+
只允许一个固定 PRD 文件:
|
|
502
|
+
|
|
503
|
+
```json
|
|
504
|
+
{
|
|
505
|
+
"version": 1,
|
|
506
|
+
"artifacts": {
|
|
507
|
+
"prd": {
|
|
508
|
+
"phases": ["discuss"],
|
|
509
|
+
"roles": ["root"],
|
|
510
|
+
"paths": ["docs/prd/current.md"]
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
允许按命名规则新建多个 PRD:
|
|
517
|
+
|
|
518
|
+
```json
|
|
519
|
+
{
|
|
520
|
+
"version": 1,
|
|
521
|
+
"artifacts": {
|
|
522
|
+
"prd": {
|
|
523
|
+
"phases": ["discuss"],
|
|
524
|
+
"roles": ["root"],
|
|
525
|
+
"paths": ["docs/prd/PRD-*.md"]
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
允许 research 在 discuss 和 plan 写文档目录:
|
|
532
|
+
|
|
533
|
+
```json
|
|
534
|
+
{
|
|
535
|
+
"version": 1,
|
|
536
|
+
"artifacts": {
|
|
537
|
+
"research": {
|
|
538
|
+
"phases": ["discuss", "orchestrating/plan"],
|
|
539
|
+
"roles": ["root", "worker"],
|
|
540
|
+
"paths": ["docs/research/**/*.md", ".scratch/research/**"]
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
#### 相关工具与命令
|
|
547
|
+
|
|
548
|
+
- `gate_artifact_write({ kind, path, content })`:root/worker 按 policy 写文件;
|
|
549
|
+
- `gate_research_dispatch({ question, purpose? })`:root-only,在 discuss/plan 派 research worker;
|
|
550
|
+
- `/gate-prd [主题]`:discuss 用户命令,加载 `gate-prd` skill 并通过 artifact policy 写 PRD;
|
|
551
|
+
- `gate-handoff`:用户级 skill,产物写入 `.scratch/handoff/`。
|
|
552
|
+
|
|
553
|
+
普通 `write`/`edit`/`apply_patch`/写型 shell 不受 artifact policy 放行,仍按 phase/role 矩阵处理。
|
|
554
|
+
|
|
555
|
+
#### 常见失败
|
|
556
|
+
|
|
557
|
+
| 错误 | 原因 |
|
|
558
|
+
|---|---|
|
|
559
|
+
| `artifact-policy-invalid` | policy 缺失、JSON 无效、version 不是 1、字段非法 |
|
|
560
|
+
| `artifact-denied` | kind 不存在、phase 不匹配、role 不匹配、path 不在白名单、blocked、保留路径、越界或 symlink |
|
|
561
|
+
| `subagents-unavailable` | research dispatch 时 `ctx.subagents` 不可用 |
|
|
562
|
+
| `wrong-phase` | research dispatch 不在 discuss 或 `orchestrating/plan` |
|
|
563
|
+
|
|
564
|
+
修改 policy 后不需要重启 DSH;每次 artifact 写入都会重新读取文件。
|
|
565
|
+
|
|
566
|
+
### 插件 Config
|
|
338
567
|
|
|
339
568
|
```json
|
|
340
569
|
{ "reviewLimit": 5 }
|
|
@@ -342,6 +571,28 @@ gate_status()
|
|
|
342
571
|
|
|
343
572
|
`reviewLimit` 是每个 review 节点的最大轮次;默认 5,范围 1–20。达到上限仍有 findings 时进入 blocked。
|
|
344
573
|
|
|
574
|
+
它**不是项目文件**,而是 DSH profile 的插件 Config。当前 profile 的覆盖文件是:
|
|
575
|
+
|
|
576
|
+
```text
|
|
577
|
+
%USERPROFILE%\.dsh\profiles\<profile>\cordis.patch.yml
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
在 profile patch 中按 bundle 的 `id: gate` 覆盖:
|
|
581
|
+
|
|
582
|
+
```yaml
|
|
583
|
+
- id: gate
|
|
584
|
+
config:
|
|
585
|
+
reviewLimit: 8
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
- 没有 `gate` 条目时使用默认值 5;
|
|
589
|
+
- 修改后重启 DSH 才生效;
|
|
590
|
+
- 不要修改 `packages/gate/cordis.patch.yml` 来配置已有 profile;那是 bundle 默认层,profile patch 会覆盖它;
|
|
591
|
+
- 如果使用自定义 Agent Preset,配置应放在该 preset 中对应的 `dsh-gate-agent` 条目。
|
|
592
|
+
|
|
593
|
+
`.agents/gate/artifact-policy.json` 是项目级 artifact 白名单,与这里的插件 Config 是两套配置。
|
|
594
|
+
|
|
595
|
+
|
|
345
596
|
## 使用边界
|
|
346
597
|
|
|
347
598
|
- 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 path11 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,331 @@ 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
|
+
|
|
1624
|
+
// src/init-manifest.ts
|
|
1625
|
+
import { createHash } from "node:crypto";
|
|
1626
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "node:fs";
|
|
1627
|
+
import path10 from "node:path";
|
|
1628
|
+
var INIT_MANIFEST_PATH = ".agents/gate/init-manifest.json";
|
|
1629
|
+
var ARTIFACT_POLICY_PATH2 = ".agents/gate/artifact-policy.json";
|
|
1630
|
+
var AGENTS_BLOCK = /<!-- gate-notes:start -->[\s\S]*?<!-- gate-notes:end -->/u;
|
|
1631
|
+
function sha256Bytes(value) {
|
|
1632
|
+
return `sha256:${createHash("sha256").update(value).digest("hex")}`;
|
|
1633
|
+
}
|
|
1634
|
+
function sha256File(file) {
|
|
1635
|
+
try {
|
|
1636
|
+
return sha256Bytes(readFileSync4(file));
|
|
1637
|
+
} catch {
|
|
1638
|
+
return null;
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
function packageVersion() {
|
|
1642
|
+
try {
|
|
1643
|
+
const pkg = JSON.parse(readFileSync4(path10.join(PACKAGE_ROOT, "package.json"), "utf8"));
|
|
1644
|
+
return typeof pkg.version === "string" ? pkg.version : "unknown";
|
|
1645
|
+
} catch {
|
|
1646
|
+
return "unknown";
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
function currentAgentsBlock(root) {
|
|
1650
|
+
try {
|
|
1651
|
+
const text = readFileSync4(path10.join(root, "AGENTS.md"), "utf8");
|
|
1652
|
+
return AGENTS_BLOCK.exec(text)?.[0] ?? null;
|
|
1653
|
+
} catch {
|
|
1654
|
+
return null;
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
function readManifest(root) {
|
|
1658
|
+
try {
|
|
1659
|
+
const value = JSON.parse(readFileSync4(path10.join(root, INIT_MANIFEST_PATH), "utf8"));
|
|
1660
|
+
if (value === null || typeof value !== "object" || value.version !== 1) return null;
|
|
1661
|
+
if (typeof value.createdAt !== "string" || typeof value.gateVersion !== "string") return null;
|
|
1662
|
+
if (value.immutable === null || typeof value.immutable !== "object" || Array.isArray(value.immutable)) return null;
|
|
1663
|
+
if (value.managed === null || typeof value.managed !== "object" || Array.isArray(value.managed)) return null;
|
|
1664
|
+
if (value.notes === null || typeof value.notes !== "object" || Array.isArray(value.notes)) return null;
|
|
1665
|
+
return value;
|
|
1666
|
+
} catch {
|
|
1667
|
+
return null;
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
function immutablePaths() {
|
|
1671
|
+
return [
|
|
1672
|
+
...NOTES_CI_SCRIPTS.map((name) => `.agents/gate/ci/${name}`),
|
|
1673
|
+
".agents/gate/verify-notes.mjs"
|
|
1674
|
+
];
|
|
1675
|
+
}
|
|
1676
|
+
function provisionInitManifest(root, packageScript) {
|
|
1677
|
+
const previous = readManifest(root);
|
|
1678
|
+
const createdAt = typeof previous?.createdAt === "string" ? previous.createdAt : (/* @__PURE__ */ new Date()).toISOString();
|
|
1679
|
+
const immutable = {};
|
|
1680
|
+
for (const relative of immutablePaths()) {
|
|
1681
|
+
const hash = sha256File(path10.join(root, relative));
|
|
1682
|
+
if (hash !== null) immutable[relative] = hash;
|
|
1683
|
+
}
|
|
1684
|
+
const policyFile = path10.join(root, ARTIFACT_POLICY_PATH2);
|
|
1685
|
+
const policyHash = sha256File(policyFile) ?? sha256Bytes("");
|
|
1686
|
+
const block = currentAgentsBlock(root);
|
|
1687
|
+
let pkgScript = null;
|
|
1688
|
+
try {
|
|
1689
|
+
const pkg = JSON.parse(readFileSync4(path10.join(root, "package.json"), "utf8"));
|
|
1690
|
+
pkgScript = typeof pkg.scripts?.["verify-notes"] === "string" ? pkg.scripts["verify-notes"] : null;
|
|
1691
|
+
} catch {
|
|
1692
|
+
}
|
|
1693
|
+
const manifest = {
|
|
1694
|
+
version: 1,
|
|
1695
|
+
gateVersion: packageVersion(),
|
|
1696
|
+
createdAt,
|
|
1697
|
+
immutable,
|
|
1698
|
+
managed: {
|
|
1699
|
+
artifactPolicy: { path: ARTIFACT_POLICY_PATH2, schemaVersion: 1, sha256: policyHash },
|
|
1700
|
+
agentsBlockHash: block === null ? null : sha256Bytes(block),
|
|
1701
|
+
packageScript: pkgScript ?? packageScript,
|
|
1702
|
+
workflow: ".github/workflows/verify-notes.yml"
|
|
1703
|
+
},
|
|
1704
|
+
notes: { path: ".agents/notes", lifecycles: ["proposed", "implemented", "rejected", "archived"] }
|
|
1705
|
+
};
|
|
1706
|
+
mkdirSync5(path10.dirname(path10.join(root, INIT_MANIFEST_PATH)), { recursive: true });
|
|
1707
|
+
writeFileSync5(path10.join(root, INIT_MANIFEST_PATH), `${JSON.stringify(manifest, null, 2)}
|
|
1708
|
+
`);
|
|
1709
|
+
return { path: INIT_MANIFEST_PATH, manifest, status: previous === null ? "created" : "updated" };
|
|
1710
|
+
}
|
|
1711
|
+
function verifyInit(root) {
|
|
1712
|
+
const manifest = readManifest(root);
|
|
1713
|
+
const issues = [];
|
|
1714
|
+
const immutable = [];
|
|
1715
|
+
for (const relative of immutablePaths()) {
|
|
1716
|
+
const actual = sha256File(path10.join(root, relative));
|
|
1717
|
+
const expected = manifest?.immutable?.[relative];
|
|
1718
|
+
if (actual === null) {
|
|
1719
|
+
immutable.push({ path: relative, status: manifest === null ? "unknown" : "missing" });
|
|
1720
|
+
if (manifest !== null) issues.push({ severity: "error", code: "immutable-missing", message: `${relative} \u7F3A\u5931` });
|
|
1721
|
+
} else if (manifest === null) {
|
|
1722
|
+
immutable.push({ path: relative, status: "unknown" });
|
|
1723
|
+
} else if (expected === void 0) {
|
|
1724
|
+
immutable.push({ path: relative, status: "missing" });
|
|
1725
|
+
issues.push({ severity: "error", code: "immutable-manifest-missing", message: `${relative} \u4E0D\u5728 init-manifest.immutable` });
|
|
1726
|
+
} else if (actual !== expected) {
|
|
1727
|
+
immutable.push({ path: relative, status: "modified" });
|
|
1728
|
+
issues.push({ severity: "error", code: "immutable-modified", message: `${relative} \u4E0E init-manifest \u4E0D\u4E00\u81F4` });
|
|
1729
|
+
} else {
|
|
1730
|
+
immutable.push({ path: relative, status: "ok" });
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
const manifestExists = existsSync5(path10.join(root, INIT_MANIFEST_PATH));
|
|
1734
|
+
if (manifest === null) issues.push({ severity: "error", code: manifestExists ? "manifest-invalid" : "manifest-missing", message: manifestExists ? "init-manifest \u65E0\u6548\uFF1B\u91CD\u65B0\u8FD0\u884C /gate-init" : "init-manifest \u7F3A\u5931\uFF1B\u91CD\u65B0\u8FD0\u884C /gate-init" });
|
|
1735
|
+
let artifactPolicy = "missing";
|
|
1736
|
+
const policyFile = path10.join(root, ARTIFACT_POLICY_PATH2);
|
|
1737
|
+
if (existsSync5(policyFile)) {
|
|
1738
|
+
try {
|
|
1739
|
+
const parsed = parseArtifactPolicy(JSON.parse(readFileSync4(policyFile, "utf8")));
|
|
1740
|
+
if (parsed.policy === void 0) artifactPolicy = "invalid";
|
|
1741
|
+
else {
|
|
1742
|
+
const actualHash = sha256File(policyFile);
|
|
1743
|
+
artifactPolicy = manifest?.managed?.artifactPolicy?.sha256 !== void 0 && actualHash !== manifest.managed.artifactPolicy.sha256 ? "customized" : "ok";
|
|
1744
|
+
}
|
|
1745
|
+
} catch {
|
|
1746
|
+
artifactPolicy = "invalid";
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
if (artifactPolicy === "missing") issues.push({ severity: "error", code: "artifact-policy-missing", message: "artifact-policy.json \u7F3A\u5931" });
|
|
1750
|
+
if (artifactPolicy === "invalid") issues.push({ severity: "error", code: "artifact-policy-invalid", message: "artifact-policy.json \u65E0\u6548" });
|
|
1751
|
+
const currentBlock = currentAgentsBlock(root);
|
|
1752
|
+
let agentsBlock = manifest === null ? "unknown" : "missing";
|
|
1753
|
+
if (currentBlock !== null && manifest !== null) {
|
|
1754
|
+
agentsBlock = manifest.managed?.agentsBlockHash !== null && manifest.managed?.agentsBlockHash !== void 0 && sha256Bytes(currentBlock) !== manifest.managed.agentsBlockHash ? "modified" : "ok";
|
|
1755
|
+
}
|
|
1756
|
+
if (agentsBlock === "missing") issues.push({ severity: "error", code: "agents-block-missing", message: "AGENTS.md \u7F3A\u5C11 gate managed block" });
|
|
1757
|
+
if (agentsBlock === "modified") issues.push({ severity: "warning", code: "agents-block-modified", message: "AGENTS.md managed block \u5DF2\u4FEE\u6539" });
|
|
1758
|
+
let packageScript = manifest === null ? "unknown" : "missing";
|
|
1759
|
+
let currentScript = null;
|
|
1760
|
+
try {
|
|
1761
|
+
const pkg = JSON.parse(readFileSync4(path10.join(root, "package.json"), "utf8"));
|
|
1762
|
+
currentScript = typeof pkg.scripts?.["verify-notes"] === "string" ? pkg.scripts["verify-notes"] : null;
|
|
1763
|
+
} catch {
|
|
1764
|
+
}
|
|
1765
|
+
if (currentScript === null) {
|
|
1766
|
+
packageScript = "missing";
|
|
1767
|
+
issues.push({ severity: "error", code: "package-script-missing", message: "package.json \u7F3A\u5C11 scripts.verify-notes" });
|
|
1768
|
+
} else if (manifest === null) {
|
|
1769
|
+
packageScript = "unknown";
|
|
1770
|
+
} else if (manifest.managed?.packageScript !== null && manifest.managed?.packageScript !== void 0 && currentScript !== manifest.managed.packageScript) {
|
|
1771
|
+
packageScript = "customized";
|
|
1772
|
+
issues.push({ severity: "warning", code: "package-script-customized", message: "scripts.verify-notes \u4E0E init \u65F6\u4E0D\u540C" });
|
|
1773
|
+
} else packageScript = "ok";
|
|
1774
|
+
const workflowPath = manifest?.managed?.workflow ?? ".github/workflows/verify-notes.yml";
|
|
1775
|
+
const workflow = existsSync5(path10.join(root, workflowPath)) ? "existing" : "missing";
|
|
1776
|
+
if (workflow === "missing") issues.push({ severity: "warning", code: "workflow-missing", message: `${workflowPath} \u7F3A\u5931` });
|
|
1777
|
+
let notes = "ok";
|
|
1778
|
+
for (const lifecycle of ["proposed", "implemented", "rejected", "archived"]) {
|
|
1779
|
+
if (!existsSync5(path10.join(root, ".agents", "notes", lifecycle))) {
|
|
1780
|
+
notes = "missing";
|
|
1781
|
+
issues.push({ severity: "error", code: "notes-missing", message: `.agents/notes/${lifecycle} \u7F3A\u5931` });
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
const currentVersion = packageVersion();
|
|
1785
|
+
if (manifest !== null && manifest.gateVersion !== currentVersion) {
|
|
1786
|
+
issues.push({ severity: "warning", code: "stale-init", message: `init gateVersion=${manifest.gateVersion}\uFF0C\u5F53\u524D=${currentVersion}\uFF1B\u5EFA\u8BAE\u91CD\u65B0\u8FD0\u884C /gate-init` });
|
|
1787
|
+
}
|
|
1788
|
+
const ok = !issues.some((issue) => issue.severity === "error");
|
|
1789
|
+
const lines = [
|
|
1790
|
+
`init verify\uFF1A${ok ? "ok" : "FAILED"}`,
|
|
1791
|
+
`manifest\uFF1A${manifest === null ? "missing/invalid" : "ok"}`,
|
|
1792
|
+
`gateVersion\uFF1A${manifest?.gateVersion ?? "unknown"}\uFF08current ${currentVersion}\uFF09`,
|
|
1793
|
+
`artifact-policy\uFF1A${artifactPolicy}`,
|
|
1794
|
+
`agents block\uFF1A${agentsBlock}`,
|
|
1795
|
+
`package script\uFF1A${packageScript}`,
|
|
1796
|
+
`workflow\uFF1A${workflow}`,
|
|
1797
|
+
`notes\uFF1A${notes}`,
|
|
1798
|
+
...issues.map((issue) => `${issue.severity}: ${issue.message}`)
|
|
1799
|
+
];
|
|
1800
|
+
return { ok, manifest: manifest === null ? manifestExists ? "invalid" : "missing" : "ok", immutable, artifactPolicy, agentsBlock, packageScript, workflow, notes, issues, text: lines.join("\n") };
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1489
1803
|
// src/commands.ts
|
|
1490
1804
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
1805
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
1491
1806
|
|
|
1492
1807
|
// src/command-catalog.ts
|
|
1493
1808
|
var GATE_COMMANDS = [
|
|
1494
1809
|
{ 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\
|
|
1810
|
+
{ 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
1811
|
{ 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" },
|
|
1812
|
+
{ 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
1813
|
{ 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:
|
|
1814
|
+
{ 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
1815
|
{ 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
1816
|
{ 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
1817
|
{ 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 +1870,28 @@ function registerCommands(ctx, deps) {
|
|
|
1554
1870
|
command2("gate-init", async (invocation) => {
|
|
1555
1871
|
const workspace = workspaceBoundary(ctx, invocation.agent);
|
|
1556
1872
|
if (workspace === void 0) return { kind: "error", text: "\u65E0\u6CD5\u786E\u5B9A\u5F53\u524D\u4F1A\u8BDD\u5DE5\u4F5C\u533A" };
|
|
1557
|
-
const root =
|
|
1873
|
+
const root = path11.resolve(workspace);
|
|
1558
1874
|
let provisioned;
|
|
1559
1875
|
let projectScripts;
|
|
1560
1876
|
let projectCi;
|
|
1561
1877
|
let projectGit;
|
|
1878
|
+
let artifactPolicy;
|
|
1879
|
+
let initManifest;
|
|
1562
1880
|
try {
|
|
1563
1881
|
provisioned = provisionNotes(root);
|
|
1882
|
+
artifactPolicy = provisionArtifactPolicy(root);
|
|
1564
1883
|
projectCi = provisionCi(root);
|
|
1565
1884
|
projectScripts = provisionProjectScripts(root);
|
|
1566
1885
|
projectGit = provisionGit(root);
|
|
1886
|
+
initManifest = provisionInitManifest(root, projectScripts.command);
|
|
1567
1887
|
} catch (error) {
|
|
1568
1888
|
return { kind: "error", text: `notes \u521D\u59CB\u5316\u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}` };
|
|
1569
1889
|
}
|
|
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");
|
|
1890
|
+
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\uFF0Cinit manifest \u4E3A ${initManifest.path}\uFF08${initManifest.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
1891
|
return { kind: "success", text: `Gate \u5DF2\u521D\u59CB\u5316\uFF1A${root}\uFF08phase=discuss\uFF09
|
|
1572
1892
|
notes\uFF1A${provisioned.notes}
|
|
1893
|
+
artifact policy\uFF1A${artifactPolicy.path}\uFF08${artifactPolicy.status}\uFF09
|
|
1894
|
+
init manifest\uFF1A${initManifest.path}\uFF08${initManifest.status}\uFF09
|
|
1573
1895
|
AGENTS\uFF1A${provisioned.agents}
|
|
1574
1896
|
verify-notes\uFF1A${projectScripts.command}
|
|
1575
1897
|
CI\uFF1A${projectCi.workflow}
|
|
@@ -1606,6 +1928,19 @@ ignore\uFF1A${projectGit.ignore}` };
|
|
|
1606
1928
|
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
1929
|
return { kind: "success", text: `\u8FDB\u5165 implement\uFF1A${result.state.task}${direct ? "\uFF08\u76F4\u63A5\u5B9E\u65BD\uFF09" : ""}` };
|
|
1608
1930
|
});
|
|
1931
|
+
command2("gate-prd", async (invocation) => {
|
|
1932
|
+
const gate = resolveGate(ctx, invocation.agent);
|
|
1933
|
+
const state = store.read(gate.root, invocation.agent);
|
|
1934
|
+
if (state.phase !== "discuss") return { kind: "error", text: "gate-prd \u53EA\u5728 discuss \u9636\u6BB5\u53EF\u7528" };
|
|
1935
|
+
const topic = String(invocation.rawInput ?? "").trim() || "\u5F53\u524D\u8BA8\u8BBA";
|
|
1936
|
+
let skill = "";
|
|
1937
|
+
try {
|
|
1938
|
+
skill = readFileSync5(path11.join(SKILLS, "gate-prd", "SKILL.md"), "utf8");
|
|
1939
|
+
} catch {
|
|
1940
|
+
}
|
|
1941
|
+
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");
|
|
1942
|
+
return { kind: "success", text: "\u5DF2\u8BF7\u6C42 PRD\uFF1A" + topic };
|
|
1943
|
+
});
|
|
1609
1944
|
command2("gate-fix-bug", async (invocation) => {
|
|
1610
1945
|
const brief = String(invocation.rawInput ?? "").trim();
|
|
1611
1946
|
const gate = resolveGate(ctx, invocation.agent);
|
|
@@ -1624,19 +1959,20 @@ ignore\uFF1A${projectGit.ignore}` };
|
|
|
1624
1959
|
return { kind: "success", text: `\u8FDB\u5165 fix-bug\uFF1A${scope2}` };
|
|
1625
1960
|
});
|
|
1626
1961
|
command2("gate-docs", async (invocation) => {
|
|
1962
|
+
const scope2 = String(invocation.rawInput ?? "").trim() || "\u6587\u6863\u6CBB\u7406";
|
|
1627
1963
|
const gate = resolveGate(ctx, invocation.agent);
|
|
1628
1964
|
const state = store.read(gate.root, invocation.agent);
|
|
1629
1965
|
if (state.phase !== "discuss" && state.phase !== "complete") return { kind: "error", text: `\u5F53\u524D phase \u662F ${state.phase}` };
|
|
1630
1966
|
if (state.work === "blocked" || state.blockReason !== null) return { kind: "error", text: `\u5F53\u524D\u88AB blocked\uFF1A${state.blockReason ?? "unknown"}\uFF1B\u5148 /gate-resume` };
|
|
1631
1967
|
state.phase = "docs";
|
|
1632
|
-
state.task =
|
|
1968
|
+
state.task = scope2;
|
|
1633
1969
|
state.work = "active";
|
|
1634
1970
|
state.blockReason = null;
|
|
1635
1971
|
state.blockedFrom = null;
|
|
1636
1972
|
state.node = null;
|
|
1637
1973
|
store.write(gate.root, invocation.agent, state);
|
|
1638
|
-
wake(invocation, state,
|
|
1639
|
-
return { kind: "success", text:
|
|
1974
|
+
wake(invocation, state, `Gate \u5DF2\u8FDB\u5165 docs\uFF1A${scope2}\u3002`, "gate-docs");
|
|
1975
|
+
return { kind: "success", text: `\u8FDB\u5165 docs\uFF1A${scope2}` };
|
|
1640
1976
|
});
|
|
1641
1977
|
command2("gate-implement-review", async (invocation) => {
|
|
1642
1978
|
const brief = String(invocation.rawInput ?? "").trim();
|
|
@@ -1766,8 +2102,8 @@ ${JSON.stringify(check.results, null, 2)}` };
|
|
|
1766
2102
|
const node = resolveNodeBinary();
|
|
1767
2103
|
if (node === void 0) return { kind: "error", text: "notes-runtime-unavailable" };
|
|
1768
2104
|
try {
|
|
1769
|
-
const script =
|
|
1770
|
-
const output = execFileSync3(node, [script, "--root",
|
|
2105
|
+
const script = path11.join(NOTES_SKILL_DIR, "scripts", "archive-agent-note.ts");
|
|
2106
|
+
const output = execFileSync3(node, [script, "--root", path11.join(gate.root, ".agents", "notes"), ...args], { cwd: gate.root, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
1771
2107
|
return { kind: "success", text: String(output).trim() || "Note \u5DF2\u5F52\u6863" };
|
|
1772
2108
|
} catch (error) {
|
|
1773
2109
|
return { kind: "error", text: String(error?.stderr ?? error?.message ?? error).trim() };
|
|
@@ -1778,11 +2114,11 @@ ${JSON.stringify(check.results, null, 2)}` };
|
|
|
1778
2114
|
const raw = String(invocation.rawInput ?? "").trim();
|
|
1779
2115
|
const args = raw === "" ? [] : raw.split(/\s+/u);
|
|
1780
2116
|
const target = args[0] ?? "board.html";
|
|
1781
|
-
const title = args[1] ?? `${
|
|
2117
|
+
const title = args[1] ?? `${path11.basename(gate.root)} \u51B3\u7B56\u770B\u677F`;
|
|
1782
2118
|
const node = resolveNodeBinary();
|
|
1783
2119
|
if (node === void 0) return { kind: "error", text: "notes-runtime-unavailable" };
|
|
1784
2120
|
try {
|
|
1785
|
-
const script =
|
|
2121
|
+
const script = path11.join(NOTES_SKILL_DIR, "scripts", "build-board.ts");
|
|
1786
2122
|
const output = execFileSync3(node, [script, "--init", target, title], { cwd: gate.root, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
1787
2123
|
return { kind: "success", text: String(output).trim() || `\u51B3\u7B56\u770B\u677F\u5DF2\u751F\u6210\uFF1A${target}` };
|
|
1788
2124
|
} catch (error) {
|
|
@@ -1794,8 +2130,8 @@ ${JSON.stringify(check.results, null, 2)}` };
|
|
|
1794
2130
|
const node = resolveNodeBinary();
|
|
1795
2131
|
if (node === void 0) return { kind: "error", text: "notes-runtime-unavailable" };
|
|
1796
2132
|
try {
|
|
1797
|
-
const script =
|
|
1798
|
-
const output = execFileSync3(node, [script, "--root",
|
|
2133
|
+
const script = path11.join(NOTES_SKILL_DIR, "scripts", "verify-archived-agent-notes.ts");
|
|
2134
|
+
const output = execFileSync3(node, [script, "--root", path11.join(gate.root, ".agents", "notes"), "--write"], { cwd: gate.root, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
1799
2135
|
return { kind: "success", text: String(output).trim() || "archived manifest \u5DF2\u5199\u5165" };
|
|
1800
2136
|
} catch (error) {
|
|
1801
2137
|
return { kind: "error", text: String(error?.stderr ?? error?.message ?? error).trim() };
|
|
@@ -1863,8 +2199,8 @@ function registerStartPhase(ctx, deps) {
|
|
|
1863
2199
|
}
|
|
1864
2200
|
|
|
1865
2201
|
// src/tools/record.ts
|
|
1866
|
-
import { createHash } from "node:crypto";
|
|
1867
|
-
import { existsSync as
|
|
2202
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
2203
|
+
import { existsSync as existsSync6, lstatSync as lstatSync5, readFileSync as readFileSync6 } from "node:fs";
|
|
1868
2204
|
function registerRecord(ctx, deps) {
|
|
1869
2205
|
const { store, reviewLimit, wake } = deps;
|
|
1870
2206
|
ctx.tools.register({
|
|
@@ -2070,12 +2406,12 @@ function registerRecord(ctx, deps) {
|
|
|
2070
2406
|
for (const item of evidence) {
|
|
2071
2407
|
const target = classifyPath(gate.workspace, item);
|
|
2072
2408
|
try {
|
|
2073
|
-
if (target.outside || !
|
|
2409
|
+
if (target.outside || !existsSync6(target.absolute) || !lstatSync5(target.absolute).isFile()) {
|
|
2074
2410
|
invalidEvidence.push(item);
|
|
2075
2411
|
continue;
|
|
2076
2412
|
}
|
|
2077
|
-
const bytes =
|
|
2078
|
-
records.push({ path: target.relative, size: bytes.length, sha256:
|
|
2413
|
+
const bytes = readFileSync6(target.absolute);
|
|
2414
|
+
records.push({ path: target.relative, size: bytes.length, sha256: createHash2("sha256").update(bytes).digest("hex") });
|
|
2079
2415
|
} catch {
|
|
2080
2416
|
invalidEvidence.push(item);
|
|
2081
2417
|
}
|
|
@@ -2193,8 +2529,8 @@ function registerStatus(ctx, deps) {
|
|
|
2193
2529
|
}
|
|
2194
2530
|
|
|
2195
2531
|
// src/tools/notes.ts
|
|
2196
|
-
import { existsSync as
|
|
2197
|
-
import
|
|
2532
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "node:fs";
|
|
2533
|
+
import path12 from "node:path";
|
|
2198
2534
|
function registerNoteTools(ctx, deps) {
|
|
2199
2535
|
const { store, reviewLimit, wake } = deps;
|
|
2200
2536
|
ctx.tools.register({
|
|
@@ -2220,7 +2556,7 @@ function registerNoteTools(ctx, deps) {
|
|
|
2220
2556
|
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
2557
|
}
|
|
2222
2558
|
let relative = String(args.path ?? "").replaceAll("\\", "/").replace(/^\.?\//u, "");
|
|
2223
|
-
if (relative === "" || relative.includes("..") ||
|
|
2559
|
+
if (relative === "" || relative.includes("..") || path12.isAbsolute(relative) || !relative.endsWith(".md")) {
|
|
2224
2560
|
return { ok: false, code: "invalid-path" };
|
|
2225
2561
|
}
|
|
2226
2562
|
let lifecycle = "proposed";
|
|
@@ -2232,16 +2568,16 @@ function registerNoteTools(ctx, deps) {
|
|
|
2232
2568
|
if (!/^(?:feature|bug-fix|simplification|architecture|process|testing)\/\d{4}-\d{2}-\d{2}-.+\.md$/u.test(relative)) {
|
|
2233
2569
|
return { ok: false, code: "invalid-path", reason: "\u9700\u8981 <class>/yyyy-mm-dd-topic.md\uFF0C\u6216 proposed|implemented/<class>/...\u3002" };
|
|
2234
2570
|
}
|
|
2235
|
-
const base =
|
|
2236
|
-
const absolute =
|
|
2571
|
+
const base = path12.join(gate.root, ".agents", "notes", String(lifecycle));
|
|
2572
|
+
const absolute = path12.resolve(base, relative);
|
|
2237
2573
|
if (!isInside(base, absolute)) return { ok: false, code: "invalid-path" };
|
|
2238
2574
|
if (pathHasSymlink(base, absolute)) return { ok: false, code: "invalid-path" };
|
|
2239
|
-
if (lifecycle === "implemented" && !
|
|
2575
|
+
if (lifecycle === "implemented" && !existsSync7(absolute)) {
|
|
2240
2576
|
return { ok: false, code: "implemented-must-exist", reason: "implemented Note \u53EA\u80FD\u539F\u5730\u7F16\u8F91\uFF0C\u4E0D\u80FD\u65B0\u5EFA\u3002" };
|
|
2241
2577
|
}
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
return { ok: true, path:
|
|
2578
|
+
mkdirSync6(path12.dirname(absolute), { recursive: true });
|
|
2579
|
+
writeFileSync6(absolute, String(args.content ?? ""));
|
|
2580
|
+
return { ok: true, path: path12.relative(gate.root, absolute).replaceAll("\\", "/"), lifecycle };
|
|
2245
2581
|
}
|
|
2246
2582
|
});
|
|
2247
2583
|
ctx.tools.register({
|
|
@@ -2264,7 +2600,7 @@ function registerNoteTools(ctx, deps) {
|
|
|
2264
2600
|
if (!check.ok) return { ...check, state: normalizeState(state) };
|
|
2265
2601
|
if (state.node === "note-lifecycle") {
|
|
2266
2602
|
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" &&
|
|
2603
|
+
const implemented = state.notePaths.length > 0 && state.notePaths.every((entry) => entry.lifecycle === "implemented" && existsSync7(path12.join(gate.root, entry.path)));
|
|
2268
2604
|
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
2605
|
state.noteVerified = true;
|
|
2270
2606
|
store.write(gate.root, exec.agent, state);
|
|
@@ -2312,9 +2648,9 @@ function registerNoteTools(ctx, deps) {
|
|
|
2312
2648
|
const recovered = recoverFinalize(gate.root, sessionId);
|
|
2313
2649
|
for (const entry of state.notePaths) {
|
|
2314
2650
|
if (entry.lifecycle !== "proposed") continue;
|
|
2315
|
-
const source =
|
|
2651
|
+
const source = path12.join(gate.root, entry.path);
|
|
2316
2652
|
const targetRelative = entry.path.replaceAll("\\", "/").replace(".agents/notes/proposed/", ".agents/notes/implemented/");
|
|
2317
|
-
if (!
|
|
2653
|
+
if (!existsSync7(source) && existsSync7(path12.join(gate.root, targetRelative))) {
|
|
2318
2654
|
entry.path = targetRelative;
|
|
2319
2655
|
entry.lifecycle = "implemented";
|
|
2320
2656
|
recovered.push("adopted-target");
|
|
@@ -2355,7 +2691,10 @@ var GATE_TOOLS = [
|
|
|
2355
2691
|
{ name: "gate_note_write", summary: "planning worker \u5199 proposed Note \u6216\u539F\u5730\u66F4\u65B0 implemented Note\u3002" },
|
|
2356
2692
|
{ name: "gate_note_verify", summary: "\u5728 note-lifecycle \u4E2D\u8FD0\u884C notes \u6821\u9A8C\uFF1Broot-only\u3002" },
|
|
2357
2693
|
{ 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" }
|
|
2694
|
+
{ name: "gate_help", summary: "\u5217\u51FA gate \u547D\u4EE4\u3001\u5DE5\u5177\u548C\u5F53\u524D phase/node\uFF1B\u53EA\u8BFB\uFF0Croot-only\u3002" },
|
|
2695
|
+
{ name: "gate_artifact_write", summary: "\u6309 artifact policy \u5199\u767D\u540D\u5355\u6587\u4EF6\uFF1B\u4E0D\u8D70\u666E\u901A workspace write\u3002" },
|
|
2696
|
+
{ name: "gate_research_dispatch", summary: "root-only \u5728 discuss/plan \u6D3E research worker\uFF1Bworker \u53EA\u5199 research artifact\u3002" },
|
|
2697
|
+
{ name: "gate_init_verify", summary: "root-only \u53EA\u8BFB\u6821\u9A8C init-manifest\u3001\u751F\u6210\u7269 hash\u3001policy \u4E0E init \u7ED3\u6784\u3002" }
|
|
2359
2698
|
];
|
|
2360
2699
|
|
|
2361
2700
|
// src/tools/help.ts
|
|
@@ -2407,6 +2746,143 @@ function registerHelp(ctx, deps) {
|
|
|
2407
2746
|
});
|
|
2408
2747
|
}
|
|
2409
2748
|
|
|
2749
|
+
// src/tools/artifact.ts
|
|
2750
|
+
function registerArtifactTools(ctx, deps) {
|
|
2751
|
+
const { store } = deps;
|
|
2752
|
+
ctx.tools.register({
|
|
2753
|
+
name: "gate_artifact_write",
|
|
2754
|
+
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",
|
|
2755
|
+
parameters: {
|
|
2756
|
+
type: "object",
|
|
2757
|
+
additionalProperties: false,
|
|
2758
|
+
properties: {
|
|
2759
|
+
kind: { type: "string", description: "policy \u4E2D\u7684 artifact kind\u3002" },
|
|
2760
|
+
path: { type: "string", description: "workspace \u5185\u76F8\u5BF9\u8DEF\u5F84\u3002" },
|
|
2761
|
+
content: { type: "string" }
|
|
2762
|
+
},
|
|
2763
|
+
required: ["kind", "path", "content"]
|
|
2764
|
+
},
|
|
2765
|
+
output: {
|
|
2766
|
+
schema: { type: "object", additionalProperties: true },
|
|
2767
|
+
render: (_args, value) => [{ type: "text", text: typeof value?.text === "string" ? value.text : JSON.stringify(value) }]
|
|
2768
|
+
},
|
|
2769
|
+
async execute(args, exec) {
|
|
2770
|
+
const gate = resolveGate(ctx, exec.agent);
|
|
2771
|
+
if (gate.root === void 0) return { ok: false, code: "state-unavailable", reason: "\u65E0\u6CD5\u786E\u5B9A\u5DE5\u4F5C\u533A\u3002" };
|
|
2772
|
+
const effective = effectiveState(ctx, exec.agent, gate, store);
|
|
2773
|
+
const loaded = loadArtifactPolicy(gate.root);
|
|
2774
|
+
if (loaded.policy === void 0) return { ok: false, code: "artifact-policy-invalid", reason: loaded.error };
|
|
2775
|
+
const allowed = artifactRuleAllows(loaded.policy, effective.state, effective.worker, String(args?.kind ?? ""));
|
|
2776
|
+
if (allowed.rule === void 0) return { ok: false, code: "artifact-denied", reason: allowed.error };
|
|
2777
|
+
const checked = checkArtifactPath(gate.root, allowed.rule, String(args?.path ?? ""), String(args?.kind ?? ""));
|
|
2778
|
+
if (checked.absolute === void 0) return { ok: false, code: "artifact-denied", reason: checked.error };
|
|
2779
|
+
const written = writeArtifact(gate.root, checked.relative ?? String(args?.path ?? ""), String(args?.content ?? ""));
|
|
2780
|
+
if (written.relative === void 0) return { ok: false, code: "artifact-write-failed", reason: written.error };
|
|
2781
|
+
return { ok: true, kind: String(args.kind), path: written.relative, bytes: Buffer.byteLength(String(args.content ?? "")) };
|
|
2782
|
+
}
|
|
2783
|
+
});
|
|
2784
|
+
}
|
|
2785
|
+
|
|
2786
|
+
// src/tools/research.ts
|
|
2787
|
+
function registerResearchDispatch(ctx, deps) {
|
|
2788
|
+
const { store } = deps;
|
|
2789
|
+
ctx.tools.register({
|
|
2790
|
+
name: "gate_research_dispatch",
|
|
2791
|
+
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",
|
|
2792
|
+
parameters: {
|
|
2793
|
+
type: "object",
|
|
2794
|
+
additionalProperties: false,
|
|
2795
|
+
properties: {
|
|
2796
|
+
question: { type: "string" },
|
|
2797
|
+
purpose: { type: "string" }
|
|
2798
|
+
},
|
|
2799
|
+
required: ["question"]
|
|
2800
|
+
},
|
|
2801
|
+
output: {
|
|
2802
|
+
schema: { type: "object", additionalProperties: true },
|
|
2803
|
+
render: (_args, value) => [{ type: "text", text: typeof value?.text === "string" ? value.text : JSON.stringify(value) }]
|
|
2804
|
+
},
|
|
2805
|
+
async execute(args, exec) {
|
|
2806
|
+
const root = rootAgentOf(ctx, exec.agent);
|
|
2807
|
+
if (root?.id !== exec.agent?.id) return { ok: false, code: "root-only", reason: "gate_research_dispatch \u53EA\u80FD\u7531 root agent \u8C03\u7528\u3002" };
|
|
2808
|
+
const gate = resolveGate(ctx, exec.agent);
|
|
2809
|
+
if (gate.root === void 0) return { ok: false, code: "state-unavailable", reason: "\u65E0\u6CD5\u786E\u5B9A\u5DE5\u4F5C\u533A\u3002" };
|
|
2810
|
+
const state = store.read(gate.root, root);
|
|
2811
|
+
if (!(state.phase === "discuss" || state.phase === "orchestrating" && state.node === "plan")) {
|
|
2812
|
+
return { ok: false, code: "wrong-phase", reason: "research dispatch \u53EA\u5141\u8BB8 discuss \u6216 orchestrating/plan\u3002" };
|
|
2813
|
+
}
|
|
2814
|
+
const loaded = loadArtifactPolicy(gate.root);
|
|
2815
|
+
if (loaded.policy === void 0) return { ok: false, code: "artifact-policy-invalid", reason: loaded.error };
|
|
2816
|
+
const allowed = artifactRuleAllows(loaded.policy, state, true, "research");
|
|
2817
|
+
if (allowed.rule === void 0) return { ok: false, code: "artifact-denied", reason: allowed.error };
|
|
2818
|
+
const subagents = typeof ctx.get === "function" ? ctx.get("subagents") : void 0;
|
|
2819
|
+
if (subagents?.startActivation === void 0 || subagents?.getProvider === void 0) {
|
|
2820
|
+
return { ok: false, code: "subagents-unavailable", reason: "ctx.subagents \u4E0D\u53EF\u7528\uFF0C\u65E0\u6CD5\u6D3E research worker\u3002" };
|
|
2821
|
+
}
|
|
2822
|
+
const provider = subagents.getProvider("spawn");
|
|
2823
|
+
if (provider === void 0) return { ok: false, code: "provider-unavailable", reason: "ctx.subagents \u6CA1\u6709 spawn provider\u3002" };
|
|
2824
|
+
const question = String(args?.question ?? "").trim();
|
|
2825
|
+
if (question === "") return { ok: false, code: "question-required", reason: "research \u9700\u8981 question\u3002" };
|
|
2826
|
+
const purpose = String(args?.purpose ?? "").trim();
|
|
2827
|
+
const prompt = [{
|
|
2828
|
+
type: "text",
|
|
2829
|
+
text: [
|
|
2830
|
+
"Research worker task. Investigate primary sources and write a Markdown report.",
|
|
2831
|
+
`Question: ${question}`,
|
|
2832
|
+
purpose === "" ? "" : `Purpose: ${purpose}`,
|
|
2833
|
+
"Use gate_artifact_write({ kind: 'research', path, content }) to save the report.",
|
|
2834
|
+
"Prefer project docs research location; otherwise .scratch/research/. Cite sources. Do not modify product code."
|
|
2835
|
+
].filter(Boolean).join("\n")
|
|
2836
|
+
}];
|
|
2837
|
+
let activation;
|
|
2838
|
+
try {
|
|
2839
|
+
activation = await subagents.startActivation({
|
|
2840
|
+
provider: "spawn",
|
|
2841
|
+
label: "gate-research",
|
|
2842
|
+
prompt,
|
|
2843
|
+
parent: exec.agent,
|
|
2844
|
+
signal: exec.signal,
|
|
2845
|
+
delivery: "caller"
|
|
2846
|
+
});
|
|
2847
|
+
const result = await activation.result;
|
|
2848
|
+
const output = result?.output;
|
|
2849
|
+
const text = typeof output === "string" ? output : JSON.stringify(output ?? result);
|
|
2850
|
+
return { ok: true, name: activation.name, text };
|
|
2851
|
+
} catch (error) {
|
|
2852
|
+
return { ok: false, code: "research-dispatch-failed", reason: error instanceof Error ? error.message : String(error) };
|
|
2853
|
+
} finally {
|
|
2854
|
+
if (activation?.dispose !== void 0) {
|
|
2855
|
+
try {
|
|
2856
|
+
await activation.dispose();
|
|
2857
|
+
} catch {
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2862
|
+
});
|
|
2863
|
+
}
|
|
2864
|
+
|
|
2865
|
+
// src/tools/init-verify.ts
|
|
2866
|
+
function registerInitVerify(ctx, _deps) {
|
|
2867
|
+
ctx.tools.register({
|
|
2868
|
+
name: "gate_init_verify",
|
|
2869
|
+
description: "root-only \u53EA\u8BFB\u6821\u9A8C /gate-init \u4EA7\u7269\uFF1Ainit-manifest\u3001immutable hash\u3001artifact policy\u3001AGENTS managed block\u3001verify-notes script\u3001workflow\u3001notes \u76EE\u5F55\u3002",
|
|
2870
|
+
parameters: { type: "object", additionalProperties: false, properties: {} },
|
|
2871
|
+
output: {
|
|
2872
|
+
schema: { type: "object", additionalProperties: true },
|
|
2873
|
+
render: (_args, value) => [{ type: "text", text: typeof value?.text === "string" ? value.text : JSON.stringify(value) }]
|
|
2874
|
+
},
|
|
2875
|
+
async execute(_args, exec) {
|
|
2876
|
+
if (rootAgentOf(ctx, exec.agent)?.id !== exec.agent?.id) {
|
|
2877
|
+
return { ok: false, code: "root-only", reason: "gate_init_verify \u53EA\u80FD\u7531 root agent \u8C03\u7528\u3002" };
|
|
2878
|
+
}
|
|
2879
|
+
const gate = resolveGate(ctx, exec.agent);
|
|
2880
|
+
if (gate.root === void 0) return { ok: false, code: "state-unavailable", reason: "\u65E0\u6CD5\u786E\u5B9A\u5DE5\u4F5C\u533A\u3002" };
|
|
2881
|
+
return verifyInit(gate.root);
|
|
2882
|
+
}
|
|
2883
|
+
});
|
|
2884
|
+
}
|
|
2885
|
+
|
|
2410
2886
|
// src/tools/index.ts
|
|
2411
2887
|
function registerTools(ctx, deps) {
|
|
2412
2888
|
registerStartPhase(ctx, deps);
|
|
@@ -2414,6 +2890,9 @@ function registerTools(ctx, deps) {
|
|
|
2414
2890
|
registerStatus(ctx, deps);
|
|
2415
2891
|
registerNoteTools(ctx, deps);
|
|
2416
2892
|
registerHelp(ctx, deps);
|
|
2893
|
+
registerArtifactTools(ctx, deps);
|
|
2894
|
+
registerResearchDispatch(ctx, deps);
|
|
2895
|
+
registerInitVerify(ctx, deps);
|
|
2417
2896
|
}
|
|
2418
2897
|
|
|
2419
2898
|
// 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
|
|
|
@@ -158,7 +158,9 @@ Status: <状态>
|
|
|
158
158
|
"verify-notes": "node .agents/gate/verify-notes.mjs"
|
|
159
159
|
```
|
|
160
160
|
|
|
161
|
-
这个启动器只调用插件内置脚本,不复制技能。
|
|
161
|
+
这个启动器只调用插件内置脚本,不复制技能。
|
|
162
|
+
|
|
163
|
+
`/gate-init` 还会生成 `.agents/gate/init-manifest.json`,记录 CI 脚本/启动器的 hash 和 init 结构。需要核对初始化是否被改坏时,用 root-only 只读工具 `gate_init_verify`;它不检查 Note 内容,也不要在每次 Note 操作后运行。`init-manifest.json` 由 git 跟踪,同内容重跑 init 时字节稳定。
|
|
162
164
|
|
|
163
165
|
- 计划完成前或提交前,运行 `npm run verify-notes`,检查 tree、format、lifecycle、archived。
|
|
164
166
|
- `/gate-init` 同时生成 `.github/workflows/verify-notes.yml` 和 `.agents/gate/ci/` 校验脚本副本;CI 使用项目内副本,不依赖插件安装路径。
|
|
@@ -33,7 +33,9 @@ Notes 读取/写入命令均接受 `--root <path>` 或 `--root=<path>`,每次
|
|
|
33
33
|
## 接入建议
|
|
34
34
|
|
|
35
35
|
- 轻量(个人/无 git):前两个脚本即可,归档封印在第一次归档后自然生效。
|
|
36
|
-
- 完整:运行 `npm run verify-notes` 跑 tree/format/lifecycle/archived 四检;该脚本由 `/gate-init` 生成,只调用插件内置脚本。gate 会话内也可用 `/gate-note-verify`。
|
|
36
|
+
- 完整:运行 `npm run verify-notes` 跑 tree/format/lifecycle/archived 四检;该脚本由 `/gate-init` 生成,只调用插件内置脚本。gate 会话内也可用 `/gate-note-verify`。
|
|
37
|
+
|
|
38
|
+
初始化结构核对用 root-only 的 `gate_init_verify`;它检查 `init-manifest.json`、生成物 hash、artifact-policy schema、AGENTS block、package script、workflow 和 notes 四目录,不检查 Note 内容,也不应随每次 Note 操作运行。
|
|
37
39
|
|
|
38
40
|
|
|
39
41
|
## 脚本路径
|
|
@@ -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.
|