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 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;不调用 gate 技能 |
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-docs` | 无 | 进入文档治理,按项目文档规范处理 |
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,不调用 gate 技能;由主代理按项目文档规范处理。
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
- 插件 Config:
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
- \u4E0D\u52A0\u8F7D gate \u6280\u80FD\uFF1B\u8303\u56F4\u7531\u6A21\u578B\u6309\u9879\u76EE\u5B9E\u9645\u6587\u6863\u9700\u8981\u5224\u65AD\u3002
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 path9 from "node:path";
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\u8F93\u5165\u8FDB\u5165 planning", hint: "<\u5177\u4F53\u5DE5\u4F5C\u4E8B\u9879\u6216\u4E0A\u4E0B\u6587\u8BED\u4E49>", when: "discuss/complete" },
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: null, when: "discuss/complete" },
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 = path9.resolve(workspace);
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 = "\u6587\u6863\u6CBB\u7406";
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, "Gate \u5DF2\u8FDB\u5165 docs\u3002", "gate-docs");
1639
- return { kind: "success", text: "\u8FDB\u5165 docs\uFF1A\u6587\u6863\u6CBB\u7406" };
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 = path9.join(NOTES_SKILL_DIR, "scripts", "archive-agent-note.ts");
1770
- const output = execFileSync3(node, [script, "--root", path9.join(gate.root, ".agents", "notes"), ...args], { cwd: gate.root, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
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] ?? `${path9.basename(gate.root)} \u51B3\u7B56\u770B\u677F`;
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 = path9.join(NOTES_SKILL_DIR, "scripts", "build-board.ts");
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 = path9.join(NOTES_SKILL_DIR, "scripts", "verify-archived-agent-notes.ts");
1798
- const output = execFileSync3(node, [script, "--root", path9.join(gate.root, ".agents", "notes"), "--write"], { cwd: gate.root, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
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 existsSync4, lstatSync as lstatSync4, readFileSync as readFileSync3 } from "node:fs";
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 || !existsSync4(target.absolute) || !lstatSync4(target.absolute).isFile()) {
2227
+ if (target.outside || !existsSync5(target.absolute) || !lstatSync5(target.absolute).isFile()) {
2074
2228
  invalidEvidence.push(item);
2075
2229
  continue;
2076
2230
  }
2077
- const bytes = readFileSync3(target.absolute);
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 existsSync5, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "node:fs";
2197
- import path10 from "node:path";
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("..") || path10.isAbsolute(relative) || !relative.endsWith(".md")) {
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 = path10.join(gate.root, ".agents", "notes", String(lifecycle));
2236
- const absolute = path10.resolve(base, relative);
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" && !existsSync5(absolute)) {
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
- mkdirSync4(path10.dirname(absolute), { recursive: true });
2243
- writeFileSync4(absolute, String(args.content ?? ""));
2244
- return { ok: true, path: path10.relative(gate.root, absolute).replaceAll("\\", "/"), lifecycle };
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" && existsSync5(path10.join(gate.root, entry.path)));
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 = path10.join(gate.root, entry.path);
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 (!existsSync5(source) && existsSync5(path10.join(gate.root, targetRelative))) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-gate-agent",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "type": "module",
5
5
  "description": "Phase/node workflow gate with built-in commands, tools, and skills.",
6
6
  "exports": {
@@ -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 to the temporary directory of the user's OS - not the current workspace.
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。之后日常由项目里的规则接管,换项目、换技术栈、或规则要改的时候再调它。
@@ -10,7 +10,7 @@ disable-model-invocation: true
10
10
 
11
11
  ## 位置与复用
12
12
 
13
- 遵循项目文档分层和既有 PRD 格式。优先更新同一产品主题已有的 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
- Spin up a **background agent** to do the research, so you keep working while it reads.
7
+ 调用 `gate_research_dispatch({ question, purpose })` 派 research worker,并等待结果。
8
8
 
9
- Its job:
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 where the repo already keeps such notes; match the existing convention, and if there is none, put it somewhere sensible and say where.
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.