@tea-agent/loop-agent 0.16.1-beta.2 → 0.16.1

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.
Files changed (94) hide show
  1. package/AGENTS.md +4 -8
  2. package/CHANGELOG.md +55 -18
  3. package/README.md +76 -299
  4. package/dist/application/evaluation/alias.js +184 -0
  5. package/dist/application/evaluation/budget.js +192 -0
  6. package/dist/application/evaluation/campaign-hash.js +47 -0
  7. package/dist/application/evaluation/campaign-matrix.js +372 -0
  8. package/dist/application/evaluation/campaign-scorecard.js +135 -0
  9. package/dist/application/evaluation/campaign.js +370 -0
  10. package/dist/application/evaluation/candidate.js +23 -6
  11. package/dist/application/evaluation/corpus-hash.js +38 -0
  12. package/dist/application/evaluation/corpus.js +56 -0
  13. package/dist/application/evaluation/experiment.js +294 -0
  14. package/dist/application/evaluation/ignition.js +198 -0
  15. package/dist/application/evaluation/integrity-audit.js +162 -0
  16. package/dist/application/evaluation/outer-loop.js +132 -0
  17. package/dist/application/evaluation/pi-cell-executor.js +39 -0
  18. package/dist/application/evaluation/private-verifier.js +46 -0
  19. package/dist/application/evaluation/promotion-policy.js +151 -0
  20. package/dist/application/evaluation/proposer.js +98 -0
  21. package/dist/application/evaluation/types.js +522 -0
  22. package/dist/cli/command-definitions.js +19 -3
  23. package/dist/commands/dag-reconcile-run.js +3 -116
  24. package/dist/commands/eval.js +1176 -13
  25. package/dist/commands/init.js +7 -1
  26. package/dist/executors/dag-pi-executor.js +4 -44
  27. package/dist/executors/pi-sdk-executor.js +3 -3
  28. package/dist/executors/shell-executor.js +1 -1
  29. package/dist/infrastructure/evaluation/alias-store.js +199 -0
  30. package/dist/infrastructure/evaluation/campaign-store.js +154 -0
  31. package/dist/infrastructure/evaluation/corpus-store.js +181 -0
  32. package/dist/infrastructure/evaluation/experiment-store.js +124 -0
  33. package/dist/infrastructure/evaluation/ignition-store.js +82 -0
  34. package/dist/infrastructure/evaluation/private-verifier-store.js +145 -0
  35. package/dist/infrastructure/evaluation/proposer-store.js +78 -0
  36. package/dist/records/promotion.js +3 -1
  37. package/dist/worker/cli.js +83 -0
  38. package/dist/worker/delivery/git-transaction.js +75 -0
  39. package/dist/worker/delivery/verification-bundle.js +13 -2
  40. package/dist/worker/feature/review.js +3 -2
  41. package/dist/worker/observe/static/dag-helpers.js +0 -62
  42. package/dist/worker/observe/static/styles.css +18 -55
  43. package/dist/worker/observe/static/views/dag.js +13 -5
  44. package/dist/worker/outcomes/adapters.js +4 -1
  45. package/dist/worker/outcomes/declared-artifacts.js +103 -0
  46. package/dist/worker/outcomes/evidence-tokens.js +29 -0
  47. package/dist/worker/outcomes/gate.js +10 -11
  48. package/dist/worker/outcomes/projector.js +30 -4
  49. package/dist/worker/outcomes/types.js +3 -0
  50. package/dist/worker/pool/reconcile.js +285 -0
  51. package/dist/worker/run-task/run-task.js +81 -4
  52. package/dist/worker/runner/run-ready.js +25 -2
  53. package/dist/worker/task-graph/ready-planner.js +14 -8
  54. package/dist/worker/task-graph/task-graph-schema.js +5 -3
  55. package/dist/workflows/dag/budget-enforcement.js +67 -0
  56. package/dist/workflows/dag/context-policy.js +137 -0
  57. package/dist/workflows/dag/failure-routing.js +7 -0
  58. package/dist/workflows/dag/frontend-implementation-contract.js +0 -77
  59. package/dist/workflows/dag/init-hybrid.js +33 -53
  60. package/dist/workflows/dag/knowledge-curator.js +3 -0
  61. package/dist/workflows/dag/node-execution.js +11 -4
  62. package/dist/workflows/dag/prompt.js +1 -1
  63. package/dist/workflows/dag/reconcile-run.js +121 -0
  64. package/dist/workflows/dag/report.js +12 -0
  65. package/dist/workflows/dag/runner.js +43 -16
  66. package/dist/workflows/dag/skill-snapshot.js +11 -7
  67. package/dist/workflows/dag/types.js +18 -1
  68. package/dist/workflows/dag/validate.js +15 -1
  69. package/docs/README.md +3 -1
  70. package/docs/architecture/runtime-boundaries.md +3 -2
  71. package/docs/init-surface.manifest.json +4 -0
  72. package/docs/local-development-environment.md +52 -0
  73. package/docs/templates/agent-dag.schema.json +0 -5
  74. package/docs/templates/agent-dag.supervised-implementation.json +23 -4
  75. package/docs/templates/branch-merge-report.md +14 -0
  76. package/docs/templates/evaluation/campaign-budget-v1.json +12 -0
  77. package/docs/templates/evaluation/campaign-dogfood-v0.json +24 -0
  78. package/docs/templates/evaluation/campaign-evidence-v1.json +44 -0
  79. package/docs/templates/evaluation/context-policy-baseline-v1.json +17 -0
  80. package/docs/templates/evaluation/context-policy-role-specialized-v1.json +28 -0
  81. package/docs/templates/evaluation/corpus-dogfood-v0.manifest.json +118 -0
  82. package/docs/templates/evaluation/matrix-dag-dry-run-v1.json +21 -0
  83. package/docs/templates/evaluation/matrix-fixture-v1.json +10 -0
  84. package/docs/templates/evaluation/private-verifier-dogfood-v0.json +16 -0
  85. package/docs/templates/product-line/AGENTS.md +1 -0
  86. package/docs/templates/product-line/README.md +17 -0
  87. package/docs/templates/product-line/acceptance.yaml +9 -0
  88. package/docs/templates/product-line/feature.yaml +11 -0
  89. package/docs/templates/product-line/task-graph.yaml +8 -0
  90. package/docs/templates/product-line/task.yaml +4 -0
  91. package/package.json +2 -1
  92. package/skills/frontend-implementation/references/node-contracts.md +3 -3
  93. package/skills/loop-agent/references/command-reference.md +5 -0
  94. package/skills/loop-agent/references/hybrid-dag.md +3 -3
@@ -1,8 +1,10 @@
1
1
  import { readdir, readFile } from "node:fs/promises";
2
2
  import { hostname } from "node:os";
3
3
  import path from "node:path";
4
+ import { isHardBudgetBreached, resolveEffectiveMaxConcurrent, } from "../../application/evaluation/budget.js";
4
5
  import { readCandidateRecord } from "../../infrastructure/evaluation/candidate-store.js";
5
6
  import { CANONICAL_TASK_ID_PATTERN, formatLocalCompactDate, } from "../../task/runtime.js";
7
+ import { assertFrozenBudget, initRunBudgetLedger, preflightBudgetOrBreach, recordFinishedNodeBudget, writeBudgetLedgerArtifacts, } from "./budget-enforcement.js";
6
8
  import { getDagRunDir, isTerminalDagRunStatus, locateDagRun, readHumanApprovalArtifact, requireActiveDagRun, } from "./lifecycle.js";
7
9
  import { moveToCompletedRunDir, moveToPausedRunDir, prepareActiveRunDir, writeRunSpec, writeRunState, } from "./run-store.js";
8
10
  import { createDagNodeExecutor } from "./executor-registry.js";
@@ -120,7 +122,7 @@ export function createInitialRunState(spec, opts, ranks, runId = opts.runId ?? "
120
122
  : {}),
121
123
  };
122
124
  }
123
- return {
125
+ const state = {
124
126
  version: 1,
125
127
  title: spec.title,
126
128
  runId,
@@ -147,6 +149,8 @@ export function createInitialRunState(spec, opts, ranks, runId = opts.runId ?? "
147
149
  }
148
150
  : {}),
149
151
  };
152
+ initRunBudgetLedger(state, spec.budget);
153
+ return state;
150
154
  }
151
155
  export function assertFrozenEvaluationBinding(spec, state) {
152
156
  const declared = spec.evaluation;
@@ -183,7 +187,7 @@ export async function runDag(spec, opts) {
183
187
  controllerVersion: runningIdentity.packageVersion,
184
188
  });
185
189
  const { ranks } = topoSortToRanks(spec);
186
- const maxConcurrent = Math.max(1, opts.maxConcurrent ?? 4);
190
+ const { maxConcurrent } = resolveEffectiveMaxConcurrent(Math.max(1, opts.maxConcurrent ?? 4), spec.budget);
187
191
  let runId = opts.runId;
188
192
  if (runId) {
189
193
  for (const warning of validateDagRunIdFormat(runId).warnings) {
@@ -268,6 +272,7 @@ export async function resumeDagRun(opts) {
268
272
  throw new Error(`dag run ${opts.runId} is already terminal (status=${state.status})`);
269
273
  }
270
274
  assertFrozenEvaluationBinding(spec, state);
275
+ assertFrozenBudget(spec.budget, state.budget, state.runId);
271
276
  // Runtime contract + controller identity must be re-verified before executing
272
277
  // any remaining node on resume; drift fails closed.
273
278
  const runningIdentity = resolveRunningControllerIdentity();
@@ -311,7 +316,7 @@ export async function resumeDagRun(opts) {
311
316
  catch (error) {
312
317
  throw new Error(`skill snapshot validation failed on resume: ${error instanceof Error ? error.message : String(error)}`);
313
318
  }
314
- const maxConcurrent = Math.max(1, opts.maxConcurrent ?? 4);
319
+ const { maxConcurrent } = resolveEffectiveMaxConcurrent(Math.max(1, opts.maxConcurrent ?? 4), spec.budget);
315
320
  state.status = "running";
316
321
  const resumedAt = new Date().toISOString();
317
322
  state.runner = {
@@ -410,23 +415,43 @@ async function executeDagCheckpoint(input) {
410
415
  tasksById,
411
416
  meta: { runDir, runId: state.runId, spec },
412
417
  }),
413
- executeScheduledNode: (nodeId, executeNode, onPause) => executeDagNode({
414
- nodeId,
415
- tasksById,
416
- state,
417
- spec,
418
- cwd,
419
- runDir,
420
- executeNode,
421
- executeDynamicNode,
422
- observer: input.observer,
423
- persistState,
424
- onPause,
425
- }),
418
+ executeScheduledNode: async (nodeId, executeNode, onPause) => {
419
+ if (isHardBudgetBreached(state.budgetLedger))
420
+ return;
421
+ const preBreach = preflightBudgetOrBreach(state);
422
+ if (preBreach && isHardBudgetBreached(state.budgetLedger)) {
423
+ await writeBudgetLedgerArtifacts(runDir, state.budgetLedger);
424
+ await persistState();
425
+ return;
426
+ }
427
+ await executeDagNode({
428
+ nodeId,
429
+ tasksById,
430
+ state,
431
+ spec,
432
+ cwd,
433
+ runDir,
434
+ executeNode,
435
+ executeDynamicNode,
436
+ observer: input.observer,
437
+ persistState,
438
+ onPause,
439
+ });
440
+ const node = state.nodes[nodeId];
441
+ if (node &&
442
+ (node.status === "FINISHED" || node.status === "ERROR")) {
443
+ recordFinishedNodeBudget(state, node);
444
+ await writeBudgetLedgerArtifacts(runDir, state.budgetLedger);
445
+ await persistState();
446
+ }
447
+ },
426
448
  });
427
449
  if (pausedByNodeId) {
428
450
  break;
429
451
  }
452
+ if (isHardBudgetBreached(state.budgetLedger)) {
453
+ break;
454
+ }
430
455
  const convergenceDecision = await runConvergencePassController({
431
456
  spec,
432
457
  state,
@@ -446,6 +471,7 @@ async function executeDagCheckpoint(input) {
446
471
  }
447
472
  state.finishedAt = new Date().toISOString();
448
473
  const runDirBeforeTransfer = runDir;
474
+ await writeBudgetLedgerArtifacts(runDir, state.budgetLedger);
449
475
  if (pausedByNodeId) {
450
476
  state.status = "paused";
451
477
  await persistState();
@@ -466,6 +492,7 @@ async function executeDagCheckpoint(input) {
466
492
  if (state.convergence) {
467
493
  relocateConvergenceArtifactPaths(state.convergence, runDirBeforeTransfer, runDir);
468
494
  }
495
+ await writeBudgetLedgerArtifacts(runDir, state.budgetLedger);
469
496
  await persistState({ allowCompletedFactsWrite: true });
470
497
  return {
471
498
  title: spec.title,
@@ -3,9 +3,9 @@ import { readFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { z } from "zod";
5
5
  import { writeTextAtomic } from "../../infrastructure/harness/atomic-write.js";
6
+ import { resolveContextPolicy } from "./context-policy.js";
6
7
  import { buildDagNodePromptEnvelope } from "./prompt.js";
7
- import { DEFAULT_SKILL_INSTRUCTION_MAX_CHARS, DEFAULT_SKILL_INSTRUCTION_TOTAL_MAX_CHARS, resolveDagSkillInstructions, } from "./skill-instructions.js";
8
- import { resolveDagNodeSkills } from "./skills.js";
8
+ import { resolveDagSkillInstructions, } from "./skill-instructions.js";
9
9
  export const SKILL_SNAPSHOT_SCHEMA_VERSION = 1;
10
10
  export const SKILL_SNAPSHOT_RESOLVER_VERSION = 1;
11
11
  export const SKILL_SNAPSHOT_REL_PATH = ".runtime/skill-snapshot.json";
@@ -117,11 +117,13 @@ function profileId(input) {
117
117
  export function buildDagSkillSnapshotProfileRequest(spec, task) {
118
118
  if (task.executor !== "pi")
119
119
  return undefined;
120
+ const policy = resolveContextPolicy(spec);
121
+ const budget = policy.resolveSkillInstructionBudget(task);
120
122
  const request = {
121
- skills: [...resolveDagNodeSkills(spec, task)],
122
- includeLearnedPatterns: task.role === "implementer",
123
- perSkillMaxChars: DEFAULT_SKILL_INSTRUCTION_MAX_CHARS,
124
- totalMaxChars: DEFAULT_SKILL_INSTRUCTION_TOTAL_MAX_CHARS,
123
+ skills: [...policy.resolveSkills(spec, task)],
124
+ includeLearnedPatterns: budget.includeLearnedPatterns,
125
+ perSkillMaxChars: budget.perSkillMaxChars,
126
+ totalMaxChars: budget.totalMaxChars,
125
127
  };
126
128
  return { id: profileId(request), ...request };
127
129
  }
@@ -511,7 +513,8 @@ function stripPromptText(instruction) {
511
513
  return metadata;
512
514
  }
513
515
  export function buildNodePromptFromSnapshot(input) {
514
- const skillNames = resolveDagNodeSkills(input.spec, input.task);
516
+ const policy = resolveContextPolicy(input.spec);
517
+ const skillNames = policy.resolveSkills(input.spec, input.task);
515
518
  const resolvedSkillInstructions = resolveNodeSkillsFromSnapshot(input.snapshot, input.spec, input.task);
516
519
  return {
517
520
  prompt: buildDagNodePromptEnvelope({
@@ -520,6 +523,7 @@ export function buildNodePromptFromSnapshot(input) {
520
523
  upstream: input.upstream,
521
524
  resolvedSkills: skillNames,
522
525
  resolvedSkillInstructions,
526
+ maxUpstreamChars: policy.resolveMaxUpstreamChars(input.task),
523
527
  }),
524
528
  resolvedSkills: resolvedSkillInstructions.map(stripPromptText),
525
529
  };
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { campaignBudgetSchema, } from "../../application/evaluation/budget.js";
2
3
  import { assertDagPromptSourceRule } from "./prompt-source.js";
3
4
  import { dagRetryPolicySchema } from "./retry-policy.js";
4
5
  export const dagComplexitySchema = z.enum(["HIGH", "MED", "LOW"]);
@@ -105,12 +106,18 @@ export const dagWritePolicySchema = z.enum([
105
106
  "exclusive",
106
107
  "none",
107
108
  ]);
109
+ export const contextPolicyIdSchema = z.enum([
110
+ "baseline-v1",
111
+ "role-specialized-v1",
112
+ ]);
108
113
  export const dagDefaultsSchema = z
109
114
  .object({
110
115
  executor: dagNodeExecutorSchema.optional(),
111
116
  model: z.string().optional(),
112
117
  piBackend: z.enum(["sdk-first", "cli-only"]).optional(),
113
118
  contextProfile: z.string().optional(),
119
+ /** Eval Lab ContextPolicy seam; default baseline-v1 when omitted. */
120
+ contextPolicyId: contextPolicyIdSchema.optional(),
114
121
  skills: z.array(z.string()).optional(),
115
122
  writePolicy: dagWritePolicySchema.optional(),
116
123
  })
@@ -267,7 +274,6 @@ export const dagTaskSchema = z.object({
267
274
  shell: dagShellConfigSchema.optional(),
268
275
  static: dagStaticConfigSchema.optional(),
269
276
  outputContract: z.string().optional(),
270
- firstProtocolLine: z.string().min(1).optional(),
271
277
  allowedPaths: z.array(z.string()).optional().default([]),
272
278
  forbiddenPaths: z.array(z.string()).optional().default([]),
273
279
  decisionGate: dagDecisionGateSchema.optional(),
@@ -301,6 +307,8 @@ export const dagEvaluationBindingSchema = z
301
307
  taskRef: z.string().min(1).optional(),
302
308
  })
303
309
  .strict();
310
+ /** Eval Lab Campaign Budget (W3.4); enforced by runner ledger when mode=hard. */
311
+ export const dagBudgetSchema = campaignBudgetSchema;
304
312
  export const dagSourceBindingSchema = z.object({
305
313
  schemaVersion: z.literal(1),
306
314
  taskId: z.string().min(1),
@@ -321,6 +329,8 @@ export const dagSpecSchema = z
321
329
  title: z.string().min(1),
322
330
  runtimeContract: dagRuntimeContractSchema.optional(),
323
331
  evaluation: dagEvaluationBindingSchema.optional(),
332
+ /** Optional hard/record-only budget; requires version 3. */
333
+ budget: dagBudgetSchema.optional(),
324
334
  sourceBinding: dagSourceBindingSchema.optional(),
325
335
  outputLanguage: dagOutputLanguageSchema.optional(),
326
336
  objective: z.string().optional(),
@@ -341,6 +351,13 @@ export const dagSpecSchema = z
341
351
  path: ["evaluation"],
342
352
  });
343
353
  }
354
+ if (spec.budget && spec.version !== 3) {
355
+ ctx.addIssue({
356
+ code: z.ZodIssueCode.custom,
357
+ message: "budget requires DagSpec version 3",
358
+ path: ["budget"],
359
+ });
360
+ }
344
361
  if (spec.runtimeContract && spec.version !== 3) {
345
362
  ctx.addIssue({
346
363
  code: z.ZodIssueCode.custom,
@@ -229,7 +229,10 @@ function validateRepairArtifactGateConfig(task, spec, issues) {
229
229
  candidate.role === "reviewer" &&
230
230
  !candidate.decisionGate?.enabled &&
231
231
  candidate.writePolicy === "read-only" &&
232
- (candidate.outputContract?.includes("VERDICT:") ?? false));
232
+ (candidate.outputContract?.includes("VERDICT:") ?? false) &&
233
+ // Format recovery / write-set format-repair nodes also emit VERDICT but
234
+ // are not the primary content review after hard verification.
235
+ !/(?:-format-repair-pi|-verdict-recovery-pi)$/.test(candidate.id));
233
236
  if (reviewCandidates.length !== 1) {
234
237
  issues.push({
235
238
  type: "invalid-repair-artifact-gate-config",
@@ -343,6 +346,17 @@ function validateTaskWritePolicy(task, spec, issues) {
343
346
  return;
344
347
  }
345
348
  for (const entry of writeSet) {
349
+ const normalizedEntry = entry.trim().replace(/\\/g, "/").replace(/^\.\//, "");
350
+ if (writePolicy === "exclusive" &&
351
+ (normalizedEntry === "**" ||
352
+ normalizedEntry === "./**" ||
353
+ normalizedEntry === "*")) {
354
+ issues.push({
355
+ type: "write-set-unbounded",
356
+ message: `task ${task.id} writeSet entry "${entry}" is unbounded; exclusive writers must use concrete paths or limited globs`,
357
+ });
358
+ continue;
359
+ }
346
360
  if (forbiddenPaths.length > 0 &&
347
361
  writeSetEntryHitsForbidden(entry, forbiddenPaths)) {
348
362
  issues.push({
package/docs/README.md CHANGED
@@ -13,6 +13,8 @@
13
13
  - `design/archive/2026-07-14-loop-agent-self-update-notifier.md` — loop-agent CLI 自更新提醒设计(已实现;历史设计说明)
14
14
  - `development-principles.md` — 仓库开发原则
15
15
  - `github-collaboration.md` — 内部研发人员的轻量 GitHub 协作指南:短分支、简短 PR、CI 与 Squash Merge
16
+ - `local-development-environment.md` — Cursor Cloud 等特定本地开发环境的已知问题与排障方法
17
+ - `branch-merge-guideline.md` — 分支合并标准流程:快速/标准/深度模式、功能保留、冲突解析、init/package 审计与 source-SHA 报告
16
18
  - `architecture/runtime-boundaries.md` — runtime 层边界与依赖方向
17
19
  - `architecture/README.md` — 架构文档目录索引与阅读路径
18
20
  - `architecture/system-overview.md` — loop-agent / agent-worker / 治理层 / 外部系统全景
@@ -84,7 +86,7 @@
84
86
  - `templates/product-line/` — 可投影的 Feature/Task/QA/Links 产品线包;配合 `agent-worker task validate-feature` 做 docs CI
85
87
  - `templates/production-readiness-checklist.md` — 低/中风险单仓库 DAG readiness 检查清单
86
88
  - `templates/init-evolution-review.md` — 初始化能力演化审查报告模板
87
- - `templates/branch-merge-report.md` — 跨分支合并的功能保留、冲突解析、init/update 与 package surface 审计模板
89
+ - `templates/branch-merge-report.md` — 跨分支合并的模式选择、功能保留、冲突解析、source drift、init/update 与 package surface 审计模板
88
90
  - `templates/adr.md` — 架构决策记录(ADR)
89
91
 
90
92
  ## 维护
@@ -85,14 +85,15 @@ Governance (scripts/check-*.sh, src/governance/)
85
85
  - **职责**:TaskSpec 校验与物化、Task Pool batch/retry/morning report under `.harness/task-pool/`、失败路由,以及只读 Observe 事件/快照/UI;实际 DAG 执行通过 `LoopAgentClient` 启动已发布的 `loop-agent` 子进程。自 0.8.0 起该目录是唯一受支持的 Task Pool runtime root,旧路径不读取、不迁移、不合并、不重映射。
86
86
  - **controller identity**:写入型 Feature/batch/Task/final verification 在任何目标仓库或 Task Pool 状态写入前解析并冻结 schemaVersion 1 identity,包括 package name/version、绝对 entry/real entry、直接可执行 launch spec、binary SHA-256 和覆盖 `package.json`、`bin/**`、`dist/**`、`.agents/skills/**` 的 portable fingerprint。expected version/fingerprint 不匹配时 fail-fast;后续 spawn 不重新查询 PATH。
87
87
  - **证据传播**:canonical Worker record、Task Pool run、batch/Feature、QA/final evidence 可选携带同一 identity;只有所有相关层都省略 identity 时才按 legacy evidence 接受,部分缺失或锚点不一致会拒绝。
88
- - **允许依赖**:TaskSpec、Task Pool、observability、Node filesystem/path 与明确的 shared/task contract;它不是第二套 executor DAG kernel。
88
+ - **Worker-aware recovery**:`agent-worker task reconcile --action abandon` 由 Worker 持有 Worker run ↔ DAG run association,再调用 DAG-kernel reconcile(`src/workflows/dag/reconcile-run.ts`)并把 Task Pool 投影为 `Failed` + recovery evidence。低层 `loop-agent dag reconcile-run` 只维护 DAG lifecycle,不搜索/修改 Task Pool。应急入口 `pool mark-failed` 同属 Worker,禁止覆盖 `Done`。
89
+ - **允许依赖**:TaskSpec、Task Pool、observability、Node filesystem/path、明确的 shared/task contract,以及只读/共享的 `src/workflows/dag/**` reconcile/lifecycle helpers;它不是第二套 executor 或 DAG kernel。
89
90
  - **禁止**:in-process import `src/cli/**`、`src/commands/**` 或 `src/application/**`。
90
91
 
91
92
  ### Infrastructure / Store
92
93
 
93
94
  - **位置**:`src/infrastructure/harness/**`(按计划逐步引入);过渡期部分逻辑仍在 `src/workflows/dag/lifecycle.ts`、`src/records/**`。
94
95
  - **职责**:`.harness/tasks`、`.harness/dag-runs`、`.harness/runs`、loop state 的集中读写;completed run facts 只读约束。DAG run 自有的 `.runtime/skill-snapshot.json` 随 lifecycle 目录整体迁移,state 只保存相对 ref 和 hash,不保存 active/paused/completed 绝对路径。
95
- - **DAG recovery mutation**:`dag reconcile-run` 是显式 operator 边界;默认仅检查,只有给出 action + reason 且 runner 已证明停止时才能保存原始快照、写 terminal reconciliation facts 并迁移 lifecycle。Observe、status 和 doctor 始终只读。
96
+ - **DAG recovery mutation**:`dag reconcile-run` 是显式 operator 边界;默认仅检查,只有给出 action + reason 且 runner 已证明停止时才能保存原始快照、写 terminal reconciliation facts 并迁移 lifecycle。Observe、status 和 doctor 始终只读。Task Pool 侧的 abandon/Failed 投影由 `agent-worker task reconcile` 持有,不得让 DAG kernel 反向依赖 Worker。
96
97
  - **禁止**:把 raw path mutation 扩散给 runner、loop action 或 command handler。
97
98
 
98
99
  ### Governance
@@ -11,6 +11,7 @@
11
11
  "docs/harness-methodology-tdd.md",
12
12
  "docs/harness-methodology-verification.md",
13
13
  "docs/harness-methodology-debugging.md",
14
+ "docs/local-development-environment.md",
14
15
  "docs/init-surface.manifest.json",
15
16
  "docs/architecture/runtime-boundaries.md",
16
17
  "docs/architecture/README.md",
@@ -39,6 +40,7 @@
39
40
  "docs/templates/product-line/test-plan.md",
40
41
  "docs/templates/product-line/task-graph.yaml",
41
42
  "docs/templates/product-line/task.yaml",
43
+ "docs/templates/product-line/feature.yaml",
42
44
  "docs/templates/product-line/closeout.yaml",
43
45
  "docs/templates/product-line/links.md",
44
46
  "scripts/kb-bootstrap-init-skeleton.sh",
@@ -106,6 +108,7 @@
106
108
  "docs/templates/product-line/test-plan.md",
107
109
  "docs/templates/product-line/task-graph.yaml",
108
110
  "docs/templates/product-line/task.yaml",
111
+ "docs/templates/product-line/feature.yaml",
109
112
  "docs/templates/product-line/closeout.yaml",
110
113
  "docs/templates/product-line/links.md",
111
114
  "scripts/check-skill-entry.sh",
@@ -175,6 +178,7 @@
175
178
  "docs/templates/product-line/test-plan.md": "copied",
176
179
  "docs/templates/product-line/task-graph.yaml": "copied",
177
180
  "docs/templates/product-line/task.yaml": "copied",
181
+ "docs/templates/product-line/feature.yaml": "copied",
178
182
  "docs/templates/product-line/closeout.yaml": "copied",
179
183
  "docs/templates/product-line/links.md": "copied",
180
184
  "scripts/check-skill-entry.sh": "generated",
@@ -0,0 +1,52 @@
1
+ # 本地开发环境排障
2
+
3
+ 本文记录仅与特定开发环境有关、但可能反复影响验证结果的问题。它不是通用工程规则;标准验证命令仍以 [`verification-matrix.md`](verification-matrix.md) 为准。
4
+
5
+ ## Cursor Cloud
6
+
7
+ Cursor Cloud VM 当前有两个需要特别注意的环境问题。
8
+
9
+ ### Node.js 版本
10
+
11
+ VM 默认 `node`(`/exec-daemon/node`)可能是 v22.14.0,但可选依赖 `@earendil-works/pi-ai` / `@earendil-works/pi-coding-agent` 要求 Node.js `>=22.19.0`。版本过低时,`npm install` / `npm ci` 可能跳过这些依赖,随后 `npm run typecheck` 或 `npm run build` 会报告:
12
+
13
+ ```text
14
+ Cannot find module '@earendil-works/...'
15
+ ```
16
+
17
+ 在 Cursor Cloud 中执行安装或验证前,先切换到已配置的 Node.js 22:
18
+
19
+ ```bash
20
+ nvm use 22
21
+ node --version
22
+ npm ci
23
+ ```
24
+
25
+ 当前 VM 预装版本为 v22.22.2;应以 `node --version` 的实际输出为准,不依赖交互式 shell 的默认 PATH。
26
+
27
+ ### Git 提交签名导致测试超时
28
+
29
+ Cursor Cloud 的全局 Git 配置可能通过 `cursor-git-ssh-keygen` 为每次提交签名(`gpg.format=ssh`、`commit.gpgsign=true`)。该 helper 可能阻塞 7–30 秒,使会在临时仓库中频繁提交的测试(尤其 `test/worker/delivery/**`、`test/worker/feature/**`)触发 15/30 秒超时。
30
+
31
+ 运行 `npm test` 或其他 Git 密集型测试前,可仅对子进程关闭提交签名:
32
+
33
+ ```bash
34
+ export GIT_CONFIG_COUNT=1
35
+ export GIT_CONFIG_KEY_0=commit.gpgsign
36
+ export GIT_CONFIG_VALUE_0=false
37
+ npm test
38
+ ```
39
+
40
+ 也可以只在当前目标仓库设置:
41
+
42
+ ```bash
43
+ git config commit.gpgsign false
44
+ ```
45
+
46
+ 该设置只用于本地开发和测试稳定性,不应写入仓库代码、模板或发布包配置。
47
+
48
+ ## 维护规则
49
+
50
+ - 仅把可复现、会反复影响开发或验证的环境问题写入本文。
51
+ - 通用且跨环境成立的要求应写入开发原则或验证矩阵,不应堆在本页。
52
+ - 问题消失或基础镜像修复后,应删除过期说明,避免把临时环境状态长期固化为项目规则。
@@ -366,11 +366,6 @@
366
366
  "type": "string",
367
367
  "minLength": 1
368
368
  },
369
- "firstProtocolLine": {
370
- "type": "string",
371
- "minLength": 1,
372
- "description": "Optional protocol prefix whose first matching Pi assistant-output line is promoted to the canonical first line. Missing matches are not synthesized."
373
- },
374
369
  "allowedPaths": {
375
370
  "type": "array",
376
371
  "items": { "type": "string", "minLength": 1 },
@@ -542,11 +542,30 @@
542
542
  "subtask_prompt_markdown": "./agent-dag-review-verdict.prompt.md"
543
543
  },
544
544
  {
545
- "id": "review-gate-shell",
545
+ "id": "review-verdict-recovery-pi",
546
546
  "depends_on": [
547
547
  "review-pi"
548
548
  ],
549
549
  "complexity": "LOW",
550
+ "executor": "pi",
551
+ "role": "reviewer",
552
+ "writePolicy": "read-only",
553
+ "allowedPaths": [
554
+ "**"
555
+ ],
556
+ "forbiddenPaths": [
557
+ ".harness/**",
558
+ "artifacts/**"
559
+ ],
560
+ "outputContract": "Plain Markdown whose first non-empty line is exactly VERDICT: pass or VERDICT: request-revision, followed by the original review findings without substantive changes. No file writes.",
561
+ "subtask_prompt": "Normalize the output format of review-pi; this is the single read-only format-recovery attempt for the review verdict protocol. Preserve an existing valid VERDICT exactly. Do not invent VERDICT: pass from natural-language approval. If ambiguous, emit VERDICT: request-revision. Do not re-review code or edit files."
562
+ },
563
+ {
564
+ "id": "review-gate-shell",
565
+ "depends_on": [
566
+ "review-verdict-recovery-pi"
567
+ ],
568
+ "complexity": "LOW",
550
569
  "executor": "shell",
551
570
  "role": "verifier",
552
571
  "writePolicy": "read-only",
@@ -557,11 +576,11 @@
557
576
  ".harness/**",
558
577
  "artifacts/**"
559
578
  ],
560
- "outputContract": "Deterministic review verdict gate: exit 0 only when review-pi first non-empty assistant output line is pass.",
561
- "subtask_prompt": "Deterministic gate: block decision gate unless review-pi emitted VERDICT: pass.",
579
+ "outputContract": "Deterministic review verdict gate: exit 0 only when review-verdict-recovery-pi first non-empty assistant output line is pass.",
580
+ "subtask_prompt": "Deterministic gate: block decision gate unless review-verdict-recovery-pi emitted VERDICT: pass.",
562
581
  "shell": {
563
582
  "verdictGate": {
564
- "fromNodeId": "review-pi",
583
+ "fromNodeId": "review-verdict-recovery-pi",
565
584
  "accept": [
566
585
  "VERDICT: pass"
567
586
  ],
@@ -6,6 +6,8 @@ Target before merge: `<full SHA>`
6
6
  Source: `<source-ref>@<full SHA>`
7
7
  Merge base: `<full SHA>`
8
8
  Merge commit: `<full SHA or follow-up commit reference>`
9
+ Merge mode: `quick | standard | deep`
10
+ Mode rationale: `<why this mode is sufficient>`
9
11
 
10
12
  ## Naming Convention
11
13
 
@@ -18,6 +20,12 @@ Use `YYYY-MM-DD-origin-<source>-into-<target>-<source-short-sha>.md` under `docs
18
20
  - Predicted conflicts: `<count and files>`.
19
21
  - Baseline verification: `<commands and results>`.
20
22
 
23
+ ## Final Source Drift Check
24
+
25
+ - Final fetch: `<command and timestamp>`.
26
+ - Final source tip: `<full SHA>`.
27
+ - Source advanced during work: `<no | yes, with re-audit details>`.
28
+
21
29
  ## Incoming Capabilities
22
30
 
23
31
  | Capability | Key files / contracts | Expected user or runtime result | Verification |
@@ -80,6 +88,12 @@ Record overlapping or high-risk files that Git merged without conflict. These ar
80
88
 
81
89
  Add focused tests and real init/update/browser/CLI smokes required by the merged capabilities.
82
90
 
91
+ ## Mode Completion Check
92
+
93
+ - Required gates for selected mode: `<list>`.
94
+ - Passed gates: `<list>`.
95
+ - Any mode upgrade during execution: `<no | from quick/standard to standard/deep, with reason>`.
96
+
83
97
  ## Residual Risk
84
98
 
85
99
  - `<unverified external environment, performance, migration, or compatibility risk>`
@@ -0,0 +1,12 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "mode": "hard",
4
+ "limits": {
5
+ "maxTokens": 2000000,
6
+ "maxWallTimeMs": 86400000,
7
+ "maxExecutorCalls": 500,
8
+ "maxRepairPasses": 50,
9
+ "maxConcurrency": 2,
10
+ "maxContextChars": 120000
11
+ }
12
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "campaignId": "camp-dogfood-context-policy-ab",
4
+ "createdAt": "2026-07-19T00:00:00.000Z",
5
+ "status": "prepared",
6
+ "policyVersion": "promotion-policy-v1",
7
+ "incumbentCandidateId": "REPLACE_INCUMBENT_CANDIDATE_ID",
8
+ "challengerCandidateIds": ["REPLACE_CHALLENGER_CANDIDATE_ID"],
9
+ "corpusId": "corpus-dogfood-v0",
10
+ "corpusHash": "REPLACE_AFTER_CORPUS_REGISTER",
11
+ "privateVerifierId": "pv-dogfood-v0",
12
+ "privateVerifierManifestHash": "REPLACE_AFTER_PRIVATE_VERIFIER_REGISTER",
13
+ "budget": {
14
+ "schemaVersion": 1,
15
+ "mode": "hard",
16
+ "limits": {
17
+ "maxExecutorCalls": 500,
18
+ "maxWallTimeMs": 86400000,
19
+ "maxRepairPasses": 50,
20
+ "maxConcurrency": 2
21
+ }
22
+ },
23
+ "notes": "M4 dogfood: single-variable Context Policy A/B. Replace candidate/corpus/privateVerifier hashes after register. held_out cells are promotion_gate_only."
24
+ }
@@ -0,0 +1,44 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "campaignId": "campaign-dogfood-v0",
4
+ "challengerCandidateId": "cand-challenger-example",
5
+ "cells": [
6
+ {
7
+ "candidateId": "cand-baseline-example",
8
+ "taskRef": "synthetic/private-1",
9
+ "seed": 1,
10
+ "split": "private",
11
+ "verifyPassed": false
12
+ },
13
+ {
14
+ "candidateId": "cand-challenger-example",
15
+ "taskRef": "synthetic/private-1",
16
+ "seed": 1,
17
+ "split": "private",
18
+ "verifyPassed": true
19
+ },
20
+ {
21
+ "candidateId": "cand-baseline-example",
22
+ "taskRef": "synthetic/heldout-1",
23
+ "seed": 1,
24
+ "split": "held_out",
25
+ "verifyPassed": true
26
+ },
27
+ {
28
+ "candidateId": "cand-challenger-example",
29
+ "taskRef": "synthetic/heldout-1",
30
+ "seed": 1,
31
+ "split": "held_out",
32
+ "verifyPassed": true
33
+ }
34
+ ],
35
+ "diffs": [],
36
+ "verifyCommands": {
37
+ "incumbent": ["npm test", "npm run typecheck"],
38
+ "challenger": ["npm test", "npm run typecheck"]
39
+ },
40
+ "budgetStatus": {
41
+ "incumbent": "ok",
42
+ "challenger": "ok"
43
+ }
44
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "policyId": "baseline-v1",
4
+ "description": "Current DAG context assembly: shared upstream char budget, role skill defaults, learned patterns only for implementer.",
5
+ "knobs": {
6
+ "maxUpstreamCharsByRole": {
7
+ "default": 2000
8
+ },
9
+ "includeLearnedPatternsRoles": ["implementer"],
10
+ "perSkillMaxCharsByRole": {
11
+ "default": 3000
12
+ },
13
+ "totalMaxCharsByRole": {
14
+ "default": 12000
15
+ }
16
+ }
17
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "policyId": "role-specialized-v1",
4
+ "description": "Role-specialized upstream and skill-instruction budgets; skill names still resolve via baseline merge order.",
5
+ "knobs": {
6
+ "maxUpstreamCharsByRole": {
7
+ "default": 2000,
8
+ "scout": 1200,
9
+ "reviewer": 1200,
10
+ "implementer": 3000,
11
+ "verifier": 1600,
12
+ "closeout": 1600,
13
+ "planner": 2000,
14
+ "supervisor": 2000
15
+ },
16
+ "includeLearnedPatternsRoles": ["implementer", "closeout"],
17
+ "perSkillMaxCharsByRole": {
18
+ "default": 3000,
19
+ "scout": 2500,
20
+ "implementer": 3500
21
+ },
22
+ "totalMaxCharsByRole": {
23
+ "default": 12000,
24
+ "scout": 10000,
25
+ "implementer": 14000
26
+ }
27
+ }
28
+ }