@tea-agent/loop-agent 0.25.6 → 0.26.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 (41) hide show
  1. package/AGENTS.md +2 -1
  2. package/CHANGELOG.md +27 -1
  3. package/README.md +8 -3
  4. package/dist/cli/command-definitions.js +25 -10
  5. package/dist/cli/help.js +4 -3
  6. package/dist/cli/program.js +43 -17
  7. package/dist/commands/import-prd.js +7 -2
  8. package/dist/commands/init.js +7 -5
  9. package/dist/commands/task-source-prepare.js +468 -0
  10. package/dist/executors/dag-pi-executor.js +66 -25
  11. package/dist/executors/model-routing.js +34 -18
  12. package/dist/executors/shell-write-guard.js +161 -25
  13. package/dist/governance/manifest-types.js +33 -5
  14. package/dist/task/source-prepare/build-draft.js +215 -0
  15. package/dist/task/source-prepare/completeness.js +195 -0
  16. package/dist/task/source-prepare/index.js +7 -0
  17. package/dist/task/source-prepare/parse-intent.js +373 -0
  18. package/dist/task/source-prepare/path-policy.js +197 -0
  19. package/dist/task/source-prepare/prepare.js +506 -0
  20. package/dist/task/source-prepare/reference-integrity.js +274 -0
  21. package/dist/task/source-prepare/types.js +7 -0
  22. package/dist/task/task-demand-routing.js +3 -1
  23. package/dist/worker/console/chat/model-resolver.js +15 -3
  24. package/dist/worker/observe/static/constants.js +3 -2
  25. package/dist/worker/observe/static/dag-model.js +1 -0
  26. package/dist/worker/observe/static/styles.css +182 -42
  27. package/dist/workflows/dag/lifecycle.js +40 -30
  28. package/dist/workflows/dag/node-execution.js +13 -0
  29. package/dist/workflows/dag/types.js +59 -19
  30. package/docs/templates/harness.schema.json +29 -7
  31. package/docs/templates/init-managed-agents.md +10 -5
  32. package/harness.json +1 -2
  33. package/package.json +1 -1
  34. package/skills/loop-agent/SKILL.md +5 -2
  35. package/skills/loop-agent/references/command-reference.md +17 -15
  36. package/skills/loop-agent/references/harness-policy.md +3 -4
  37. package/skills/loop-agent/references/hybrid-dag.md +2 -2
  38. package/skills/loop-agent/references/model-routing.md +2 -0
  39. package/skills/loop-agent/references/post-implementation-and-patterns.md +1 -1
  40. package/skills/loop-agent/references/source-and-plan-practice.md +3 -2
  41. package/skills/loop-agent/references/task-workflow.md +7 -5
@@ -1,4 +1,4 @@
1
- import { access, mkdir, readFile, readdir, rename, } from "node:fs/promises";
1
+ import { access, mkdir, readFile, readdir, rename } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { hostname as localHostname } from "node:os";
4
4
  import { writeJsonAtomic, } from "../../infrastructure/harness/atomic-write.js";
@@ -170,20 +170,22 @@ export function assessDagRunLiveness(input) {
170
170
  return { status: "unknown" };
171
171
  if (runner.hostname !== (input.hostname ?? localHostname()))
172
172
  return { status: "unknown-host" };
173
- const isAlive = input.isProcessAlive ?? ((pid) => {
174
- try {
175
- process.kill(pid, 0);
176
- return true;
177
- }
178
- catch {
179
- return false;
180
- }
181
- });
173
+ const isAlive = input.isProcessAlive ??
174
+ ((pid) => {
175
+ try {
176
+ process.kill(pid, 0);
177
+ return true;
178
+ }
179
+ catch {
180
+ return false;
181
+ }
182
+ });
182
183
  if (!isAlive(runner.pid))
183
184
  return { status: "orphaned", runnerAlive: false };
184
185
  const heartbeatMs = Date.parse(runner.heartbeatAt);
185
186
  const nowMs = (input.now ?? new Date()).getTime();
186
- if (!Number.isNaN(heartbeatMs) && nowMs - heartbeatMs > (input.staleThresholdMs ?? 90_000)) {
187
+ if (!Number.isNaN(heartbeatMs) &&
188
+ nowMs - heartbeatMs > (input.staleThresholdMs ?? 90_000)) {
187
189
  return { status: "stale", runnerAlive: true };
188
190
  }
189
191
  const activeNode = Object.values(input.state.nodes).find((node) => node.status === "RUNNING");
@@ -193,20 +195,20 @@ export function assessDagRunLiveness(input) {
193
195
  if (activeNode.livenessStatus === "needs-attention") {
194
196
  return { status: "needs-attention", runnerAlive: true };
195
197
  }
196
- if (activeNode.livenessStatus === "suspected-stall"
197
- || activeNode.livenessStatus === "probing") {
198
+ if (activeNode.livenessStatus === "suspected-stall" ||
199
+ activeNode.livenessStatus === "probing") {
198
200
  return { status: "suspected-stall", runnerAlive: true };
199
201
  }
200
202
  if (activeNode.livenessStatus === "quiet") {
201
203
  return { status: "node-quiet", runnerAlive: true };
202
204
  }
203
205
  // Fall back to meaningful progress clocks (never use runner lease as progress).
204
- const meaningfulAt = activeNode.lastMeaningfulProgressAt
205
- ?? activeNode.lastProviderActivityAt
206
- ?? activeNode.lastToolActivityAt
207
- ?? activeNode.lastOutputActivityAt
208
- ?? activeNode.lastActivityAt
209
- ?? activeNode.startedAt;
206
+ const meaningfulAt = activeNode.lastMeaningfulProgressAt ??
207
+ activeNode.lastProviderActivityAt ??
208
+ activeNode.lastToolActivityAt ??
209
+ activeNode.lastOutputActivityAt ??
210
+ activeNode.lastActivityAt ??
211
+ activeNode.startedAt;
210
212
  const nodeActivityMs = Date.parse(meaningfulAt ?? "");
211
213
  if (!Number.isNaN(nodeActivityMs)) {
212
214
  const idleMs = nowMs - nodeActivityMs;
@@ -229,12 +231,12 @@ export function deriveDagRunEffectiveStatus(input) {
229
231
  if (input.lifecycle === "paused")
230
232
  return "paused";
231
233
  if (input.lifecycle === "completed") {
232
- return input.state.status === "finished" ? "finished" : "failed";
234
+ return mapTerminalDagRunEffectiveStatus(input.state.status);
233
235
  }
234
236
  if (input.state.status === "pending")
235
237
  return "pending";
236
238
  if (isTerminalDagRunStatus(input.state.status)) {
237
- return input.state.status === "finished" ? "finished" : "failed";
239
+ return mapTerminalDagRunEffectiveStatus(input.state.status);
238
240
  }
239
241
  if (input.liveness === "orphaned" || input.liveness === "stale")
240
242
  return "interrupted";
@@ -252,19 +254,20 @@ export function deriveDagRunEffectiveStatus(input) {
252
254
  }
253
255
  export function assessDagRunRecoveryEligibility(input) {
254
256
  const reasons = [];
255
- const canResume = input.lifecycle === "active"
256
- && input.state.status === "running"
257
- && Boolean(input.state.humanDecisionNodeId)
258
- && Boolean(input.hasHumanApproval);
257
+ const canResume = input.lifecycle === "active" &&
258
+ input.state.status === "running" &&
259
+ Boolean(input.state.humanDecisionNodeId) &&
260
+ Boolean(input.hasHumanApproval);
259
261
  if (!canResume)
260
262
  reasons.push("standard-resume-preconditions-not-met");
261
263
  let canReconcile = true;
262
- if (input.lifecycle === "completed" || isTerminalDagRunStatus(input.state.status)) {
264
+ if (input.lifecycle === "completed" ||
265
+ isTerminalDagRunStatus(input.state.status)) {
263
266
  canReconcile = false;
264
267
  reasons.push("run-already-terminal");
265
268
  }
266
- if (input.lifecycle === "active"
267
- && [
269
+ if (input.lifecycle === "active" &&
270
+ [
268
271
  "active",
269
272
  "node-quiet",
270
273
  "suspected-stall",
@@ -298,6 +301,13 @@ export const TERMINAL_RUN_STATUSES = new Set([
298
301
  export function isTerminalDagRunStatus(status) {
299
302
  return TERMINAL_RUN_STATUSES.has(status);
300
303
  }
304
+ function mapTerminalDagRunEffectiveStatus(status) {
305
+ if (status === "finished")
306
+ return "finished";
307
+ if (status === "partial_failed")
308
+ return "partial_failed";
309
+ return "failed";
310
+ }
301
311
  function listPendingNodeIds(state) {
302
312
  return Object.values(state.nodes)
303
313
  .filter((node) => node.status === "PENDING")
@@ -477,8 +487,8 @@ export async function listAllDagRunEntries(repoRoot) {
477
487
  located.push(...(await listRunsInLifecycleDir(repoRoot, lifecycle)));
478
488
  }
479
489
  return located.sort((left, right) => {
480
- const lifecycleCompare = DAG_LIFECYCLE_SCAN_ORDER.indexOf(left.lifecycle)
481
- - DAG_LIFECYCLE_SCAN_ORDER.indexOf(right.lifecycle);
490
+ const lifecycleCompare = DAG_LIFECYCLE_SCAN_ORDER.indexOf(left.lifecycle) -
491
+ DAG_LIFECYCLE_SCAN_ORDER.indexOf(right.lifecycle);
482
492
  if (lifecycleCompare !== 0)
483
493
  return lifecycleCompare;
484
494
  return left.runId.localeCompare(right.runId);
@@ -16,6 +16,8 @@ import { assertSkillSnapshotCoversSpec, buildNodePromptFromSnapshot, isDagSkillS
16
16
  import { resolveDagSkillInstructions, skillInstructionMetadata, } from "./skill-instructions.js";
17
17
  import { parseRepairArtifactFromText, resolveRepairTaskForGate, validateRepairArtifactScope, } from "./repair-artifact.js";
18
18
  import { resolveModelForTask, } from "./types.js";
19
+ import { loadHarnessManifest } from "../../governance/harness.js";
20
+ import { resolveExecutorThinkingMatrix } from "../../executors/model-routing.js";
19
21
  export function buildNodePrompt(spec, task, upstream, options) {
20
22
  const policy = resolveContextPolicy(spec);
21
23
  return buildDagNodePromptEnvelope({
@@ -359,6 +361,16 @@ export async function executeDagNode(input) {
359
361
  node.resolvedSkills = resolvedSkills;
360
362
  await writeNodeSkillArtifacts(runDir, nodeId, resolvedSkills);
361
363
  const model = resolveModelForTask(task, spec.executorModels);
364
+ let thinking;
365
+ if (task.executor === "pi") {
366
+ try {
367
+ const manifest = await loadHarnessManifest(cwd);
368
+ thinking = resolveExecutorThinkingMatrix(manifest.executors?.pi)[task.complexity];
369
+ }
370
+ catch {
371
+ // Invalid or missing harness preserves the previous no-override behavior.
372
+ }
373
+ }
362
374
  const retryPolicy = task.retryPolicy && isSafeReadOnlyPiRetryCandidate(task)
363
375
  ? task.retryPolicy
364
376
  : undefined;
@@ -418,6 +430,7 @@ export async function executeDagNode(input) {
418
430
  task,
419
431
  cwd,
420
432
  model,
433
+ ...(thinking ? { thinking } : {}),
421
434
  prompt: buildAttemptPrompt(task, prompt, attemptNumber, previousFailureCategory, previousProtocolReason),
422
435
  attempt: attemptNumber,
423
436
  reportActivity,
@@ -74,7 +74,9 @@ export const dagVerdictGateSchema = z.object({
74
74
  * Prefer listing the post-revision / final reviewer first when both may exist.
75
75
  */
76
76
  fallbackFromNodeIds: z
77
- .array(z.string().regex(/^[a-z][a-z0-9-]*$/, "fallbackFromNodeIds must be kebab-case"))
77
+ .array(z
78
+ .string()
79
+ .regex(/^[a-z][a-z0-9-]*$/, "fallbackFromNodeIds must be kebab-case"))
78
80
  .optional(),
79
81
  accept: z.array(z.string().min(1)).min(1),
80
82
  lineMode: z.enum(["first-non-empty", "first-verdict-line"]).optional(),
@@ -130,15 +132,20 @@ export const dagFrontendPrewriteGateSchema = z.object({
130
132
  planFallbackFromNodeIds: z.array(dagFrontendNodeIdSchema).default([]),
131
133
  reviewFromNodeId: dagFrontendNodeIdSchema,
132
134
  reviewFallbackFromNodeIds: z.array(dagFrontendNodeIdSchema).default([]),
133
- requiredRequirementIds: z.array(z.string().regex(/^(?:REQ|BR|AC)-[A-Z0-9]+(?:-[A-Z0-9]+)*$/)).default([]),
134
- allowedMockStrategies: z.array(z.enum(["native", "browser-intercept", "request-adapter", "not-needed"])).min(1),
135
+ requiredRequirementIds: z
136
+ .array(z.string().regex(/^(?:REQ|BR|AC)-[A-Z0-9]+(?:-[A-Z0-9]+)*$/))
137
+ .default([]),
138
+ allowedMockStrategies: z
139
+ .array(z.enum(["native", "browser-intercept", "request-adapter", "not-needed"]))
140
+ .min(1),
135
141
  artifactName: z.string().regex(/^[a-z0-9][a-z0-9._-]*\.json$/),
136
142
  outputDir: z.string().regex(/^[a-z0-9][a-z0-9._-]*$/),
137
143
  requireSourceFreshness: z.literal(true),
138
144
  implementationWriteSet: z.array(z.string().min(1)).min(1).optional(),
139
145
  openspecCandidatePaths: z
140
- .array(z.string().refine((candidate) => !candidate.includes("\\") &&
141
- isOpenspecSpecFilePath(candidate), "openspec candidate must be a repo-relative supported file under openspec/schemas/ or openspec/project-specs/"))
146
+ .array(z
147
+ .string()
148
+ .refine((candidate) => !candidate.includes("\\") && isOpenspecSpecFilePath(candidate), "openspec candidate must be a repo-relative supported file under openspec/schemas/ or openspec/project-specs/"))
142
149
  .default([]),
143
150
  });
144
151
  export const dagFrontendLintBaselineSchema = z
@@ -148,7 +155,8 @@ export const dagFrontendLintBaselineSchema = z
148
155
  lintEvidence: dagShellVerifyEvidenceSchema,
149
156
  })
150
157
  .strict();
151
- export const dagFrontendVerificationBundleSchema = z.object({
158
+ export const dagFrontendVerificationBundleSchema = z
159
+ .object({
152
160
  schemaVersion: z.literal(1),
153
161
  mockCommands: z.array(z.string()).default([]),
154
162
  lintCommands: z.array(z.string().min(1)).optional(),
@@ -161,7 +169,8 @@ export const dagFrontendVerificationBundleSchema = z.object({
161
169
  lintBaselineNodeId: dagFrontendNodeIdSchema.optional(),
162
170
  writerNodeIds: z.array(dagFrontendNodeIdSchema).optional(),
163
171
  mode: z.enum(["initial", "repair"]),
164
- }).superRefine((bundle, context) => {
172
+ })
173
+ .superRefine((bundle, context) => {
165
174
  const groups = [
166
175
  ["mock", bundle.mockCommands, bundle.mockEvidence],
167
176
  ["lint", bundle.lintCommands ?? [], bundle.lintEvidence],
@@ -169,23 +178,48 @@ export const dagFrontendVerificationBundleSchema = z.object({
169
178
  ["behavior", bundle.behaviorCommands, bundle.behaviorEvidence],
170
179
  ];
171
180
  for (const [name, commands, evidence] of groups) {
172
- if ((name === "mock" || name === "lint") && commands.length === 0 && !evidence)
181
+ if ((name === "mock" || name === "lint") &&
182
+ commands.length === 0 &&
183
+ !evidence)
173
184
  continue;
174
185
  if (!evidence) {
175
- context.addIssue({ code: z.ZodIssueCode.custom, path: [`${name}Evidence`], message: `${name} evidence is required when commands are configured` });
186
+ context.addIssue({
187
+ code: z.ZodIssueCode.custom,
188
+ path: [`${name}Evidence`],
189
+ message: `${name} evidence is required when commands are configured`,
190
+ });
176
191
  continue;
177
192
  }
178
193
  if (evidence.commandCount !== commands.length) {
179
- context.addIssue({ code: z.ZodIssueCode.custom, path: [`${name}Evidence`, "commandCount"], message: `${name} commandCount must match commands` });
194
+ context.addIssue({
195
+ code: z.ZodIssueCode.custom,
196
+ path: [`${name}Evidence`, "commandCount"],
197
+ message: `${name} commandCount must match commands`,
198
+ });
180
199
  }
181
200
  if (evidence.commandLabels.length !== commands.length) {
182
- context.addIssue({ code: z.ZodIssueCode.custom, path: [`${name}Evidence`, "commandLabels"], message: `${name} commandLabels must match commands` });
201
+ context.addIssue({
202
+ code: z.ZodIssueCode.custom,
203
+ path: [`${name}Evidence`, "commandLabels"],
204
+ message: `${name} commandLabels must match commands`,
205
+ });
183
206
  }
184
- if (evidence.commandTexts.length !== commands.length || evidence.commandTexts.some((command, index) => command !== commands[index])) {
185
- context.addIssue({ code: z.ZodIssueCode.custom, path: [`${name}Evidence`, "commandTexts"], message: `${name} commandTexts must exactly match commands` });
207
+ if (evidence.commandTexts.length !== commands.length ||
208
+ evidence.commandTexts.some((command, index) => command !== commands[index])) {
209
+ context.addIssue({
210
+ code: z.ZodIssueCode.custom,
211
+ path: [`${name}Evidence`, "commandTexts"],
212
+ message: `${name} commandTexts must exactly match commands`,
213
+ });
186
214
  }
187
- if (new Set(evidence.commandLabels).size !== evidence.commandLabels.length || evidence.commandLabels.some((label) => !label.trim())) {
188
- context.addIssue({ code: z.ZodIssueCode.custom, path: [`${name}Evidence`, "commandLabels"], message: `${name} commandLabels must be non-empty and unique` });
215
+ if (new Set(evidence.commandLabels).size !==
216
+ evidence.commandLabels.length ||
217
+ evidence.commandLabels.some((label) => !label.trim())) {
218
+ context.addIssue({
219
+ code: z.ZodIssueCode.custom,
220
+ path: [`${name}Evidence`, "commandLabels"],
221
+ message: `${name} commandLabels must be non-empty and unique`,
222
+ });
189
223
  }
190
224
  }
191
225
  if ((bundle.lintCommands?.length ?? 0) > 0) {
@@ -318,9 +352,12 @@ export const dagShellConfigSchema = z.object({
318
352
  commands: z.array(z.string()).default([]),
319
353
  preset: dagShellPresetSchema.optional(),
320
354
  verdictGate: dagVerdictGateSchema.optional(),
321
- projectGovernanceGate: z.object({
355
+ projectGovernanceGate: z
356
+ .object({
322
357
  contextPath: z.literal(".runtime/project-governance-context.json"),
323
- }).strict().optional(),
358
+ })
359
+ .strict()
360
+ .optional(),
324
361
  requirementCoverageGate: dagRequirementCoverageGateSchema.optional(),
325
362
  jsonArtifactGate: dagJsonArtifactGateSchema.optional(),
326
363
  frontendPrewriteGate: dagFrontendPrewriteGateSchema.optional(),
@@ -333,7 +370,8 @@ export const dagShellConfigSchema = z.object({
333
370
  frontendTestHtmlReport: dagFrontendTestHtmlReportSchema.optional(),
334
371
  backendTestPipeline: dagBackendTestPipelineSchema.optional(),
335
372
  /** JaCoCo coverage collection for backend-test (Java services). When set, node 7 dumps coverage over TCP from a JaCoCo tcpserver agent and feeds it to the L-5 dashboard. */
336
- jacocoCoverage: z.object({
373
+ jacocoCoverage: z
374
+ .object({
337
375
  /** JaCoCo tcpserver endpoint, e.g. "host:6300". */
338
376
  endpoint: z.string().min(1),
339
377
  /** Absolute path to jacococli.jar on this machine, used to convert .exec → jacoco.xml. */
@@ -342,7 +380,9 @@ export const dagShellConfigSchema = z.object({
342
380
  includes: z.string().min(1).optional().default("*"),
343
381
  /** TCP connect timeout in ms. Defaults to 5000. */
344
382
  connectTimeoutMs: z.number().int().positive().optional().default(5000),
345
- }).strict().optional(),
383
+ })
384
+ .strict()
385
+ .optional(),
346
386
  verifyEvidence: dagShellVerifyEvidenceSchema.optional(),
347
387
  repairArtifactGate: dagRepairArtifactGateSchema.optional(),
348
388
  /** fail (default): any nonzero command fails the node. record: finish node FINISHED with failure facts for downstream assess/repair. */
@@ -187,10 +187,32 @@
187
187
  "enum": ["standard-dag", "review-gated-dag", "supervised-implementation"],
188
188
  "description": "某个治理 profile 选择的 DAG 生成模板。"
189
189
  },
190
+ "executorTierModel": {
191
+ "oneOf": [
192
+ { "type": "string" },
193
+ {
194
+ "type": "object",
195
+ "additionalProperties": false,
196
+ "required": ["model"],
197
+ "properties": {
198
+ "model": {
199
+ "type": "string",
200
+ "minLength": 1,
201
+ "description": "模型 id 或 provider/model 限定引用。"
202
+ },
203
+ "thinking": {
204
+ "type": "string",
205
+ "description": "可选 Pi thinking level(如 max/low)。省略时不强制覆盖 Pi 默认。"
206
+ }
207
+ }
208
+ }
209
+ ],
210
+ "description": "executor 复杂度档模型:字符串 model id,或 { model, thinking? }。字面值 default 表示不覆盖。"
211
+ },
190
212
  "executor": {
191
213
  "type": "object",
192
214
  "additionalProperties": false,
193
- "description": "executor 设置。DAG 模型选择优先读取 LOW/MED/HIGH,其次读取 defaultModel,最后回退到 loop-agent runtime 默认矩阵。字面值 default 表示不覆盖。",
215
+ "description": "executor 设置。DAG 模型选择优先读取 LOW/MED/HIGH,其次读取 defaultModel,最后回退到 loop-agent runtime 默认矩阵。字面值 default 表示不覆盖。LOW/MED/HIGH 可为字符串或 { model, thinking? } 对象。",
194
216
  "properties": {
195
217
  "description": { "type": "string" },
196
218
  "enabled": { "type": "boolean" },
@@ -200,16 +222,16 @@
200
222
  "default": "default"
201
223
  },
202
224
  "LOW": {
203
- "type": "string",
204
- "description": "LOW 复杂度 DAG task 的模型覆盖值,优先级高于 defaultModel"
225
+ "$ref": "#/$defs/executorTierModel",
226
+ "description": "LOW 复杂度 DAG task 的模型覆盖值,优先级高于 defaultModel。可为模型字符串,或 { model, thinking? } 对象。"
205
227
  },
206
228
  "MED": {
207
- "type": "string",
208
- "description": "MED 复杂度 DAG task 的模型覆盖值,优先级高于 defaultModel"
229
+ "$ref": "#/$defs/executorTierModel",
230
+ "description": "MED 复杂度 DAG task 的模型覆盖值,优先级高于 defaultModel。可为模型字符串,或 { model, thinking? } 对象。"
209
231
  },
210
232
  "HIGH": {
211
- "type": "string",
212
- "description": "HIGH 复杂度 DAG task 的模型覆盖值,优先级高于 defaultModel"
233
+ "$ref": "#/$defs/executorTierModel",
234
+ "description": "HIGH 复杂度 DAG task 的模型覆盖值,优先级高于 defaultModel。可为模型字符串,或 { model, thinking? } 对象。"
213
235
  },
214
236
  "requiresApiKey": {
215
237
  "type": "string",
@@ -86,19 +86,24 @@ pwd → `README.md` → `harness.json` → `__LOOP_AGENT_GOVERNANCE_ROOT__/READM
86
86
 
87
87
  ```bash
88
88
  loop-agent new-task <task-id> "任务标题"
89
- # 有原始 PRD 时(推荐):loop-agent import-prd <task-id> --file <path-to-prd.md>
89
+ # 推荐默认:详细 PRD import prepare(无需手写两个 source;默认无 LLM 写 source)
90
+ loop-agent import-prd <task-id> --file <path-to-prd.md>
90
91
  # 非微小或跨会话任务(推荐):loop-agent plan create <plan-id> "<title>"
91
- # write .harness/tasks/<task-id>/source/需求.md
92
- # write .harness/tasks/<task-id>/source/执行约束.md
92
+ loop-agent task source prepare <task-id> \
93
+ --use-imported-prd \
94
+ --allowed-path "<glob>" \
95
+ --forbidden-path ".harness/**" \
96
+ --verify "typecheck:npm run typecheck" \
97
+ --apply --json
93
98
  loop-agent dag run-task <task-id> --profile auto --strict-models
94
99
  loop-agent dag validate --dag .harness/tasks/<task-id>/dag.json --strict-models --strict-governance
95
100
  loop-agent run-dag --dag .harness/tasks/<task-id>/dag.json --cwd .
96
101
  # 有 plan 时收尾:loop-agent plan complete <plan-id> --summary "..."
97
102
  ```
98
103
 
99
- `source/需求.md` 与 `source/执行约束.md` 必需;有原始 PRD 时优先用 `import-prd` 原样归档。`import-prd` / `plan create` 不是 `dag run-task` 的硬依赖。写入前同步 `task.json.allowedPaths` / `task.json.forbiddenPaths` 并审查 writer `writeSet`。
104
+ `source/需求.md` 与 `source/执行约束.md` 仍必需(M8/M9),但默认由 `task source prepare --apply` 投影生成,而不是主会话手写。有原始 PRD 时优先 `import-prd` 原样归档。`import-prd` / `plan create` 不是 `dag run-task` 的硬依赖。写入前同步 `task.json.allowedPaths` / `task.json.forbiddenPaths` 并审查 writer `writeSet`。
100
105
 
101
- 凡是影响项目公共契约、执行入口、交付流水线、自动化/治理、数据模型、安全或权限模型、跨模块行为、用户可见工作流的改动,都必须在编辑实现文件前先创建任务、写好两个 source 文件、生成 DAG,并审查 DAG/writeSet。
106
+ 凡是影响项目公共契约、执行入口、交付流水线、自动化/治理、数据模型、安全或权限模型、跨模块行为、用户可见工作流的改动,都必须在编辑实现文件前先创建任务、完成 source prepare(或等价 managed contract)、生成 DAG,并审查 DAG/writeSet。
102
107
 
103
108
  ### 任务类型路由(taskKind)
104
109
 
package/harness.json CHANGED
@@ -58,9 +58,8 @@
58
58
  "executors": {
59
59
  "pi": {
60
60
  "description": "Pi planning, review, diagnosis, and bounded writing when DAG toolProfile=write",
61
- "defaultModel": "minimax-m3",
62
61
  "LOW": "minimax-m3",
63
- "MED": "gpt-5.5",
62
+ "MED": {"model": "deepseek/deepseek-v4-flash","thinking": "max"},
64
63
  "HIGH": "gpt-5.6-sol"
65
64
  }
66
65
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tea-agent/loop-agent",
3
- "version": "0.25.6",
3
+ "version": "0.26.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "loop-agent": "bin/loop-agent.js",
@@ -27,8 +27,11 @@ Entry: routing and hard rules. Required details come from frontmatter references
27
27
 
28
28
  ```bash
29
29
  loop-agent new-task <task-id> "Title"
30
- # Prefer import-prd when a PRD exists; plan create for non-trivial work
31
- # (see references/source-and-plan-practice.md)
30
+ # PRD-first default: detailed PRD import-prd task source prepare --apply
31
+ # no hand-written/LLM source by default
32
+ loop-agent import-prd <task-id> --file <prd.md>
33
+ loop-agent task source prepare <task-id> --use-imported-prd --allowed-path "<glob>" --apply --json
34
+ # plan create for non-trivial work (see references/source-and-plan-practice.md)
32
35
  loop-agent dag run-task <task-id> --profile auto --strict-models
33
36
  # default draft: .harness/tasks/<task-id>/dag.json
34
37
  loop-agent dag validate --dag .harness/tasks/<task-id>/dag.json --strict-models --strict-governance
@@ -37,9 +37,14 @@ loop-agent doctor
37
37
 
38
38
  ```bash
39
39
  loop-agent new-task <task-id> "Task Title"
40
- # 有原始 PRD 文件时(推荐默认):
41
- # loop-agent import-prd <task-id> --file <path-to-prd.md>
42
- # source/需求.md、执行约束.md,并同步 task.json 路径边界
40
+ # 推荐默认:详细 PRD → import → prepare(无需手写两 source;默认无 LLM 写 source)
41
+ loop-agent import-prd <task-id> --file <path-to-prd.md>
42
+ loop-agent task source prepare <task-id> \
43
+ --use-imported-prd \
44
+ --allowed-path "<glob>" \
45
+ --forbidden-path ".harness/**" \
46
+ --verify "typecheck:npm run typecheck" \
47
+ --apply --json
43
48
  # 非微小 / 跨会话(推荐默认):
44
49
  # loop-agent plan create <plan-id> "<title>"
45
50
  loop-agent dag run-task <task-id> --profile auto --strict-models
@@ -168,7 +173,7 @@ loop-agent new-task <task-id> "Task Title"
168
173
  loop-agent import-prd <task-id> --file ai_workspace/loop-agent/path/to-prd.md [--name requirement] [--json]
169
174
  ```
170
175
 
171
- 把用户原始 PRD **原样复制** 到 `.harness/tasks/<task-id>/source/references/`,并写入 `source/source-manifest.json`(含 SHA-256)与 `task.json.referenceDocs`。此步骤不调用模型、不改写内容。随后再写派生的 `source/需求.md` 执行契约;冲突时以 `source/references/*` 为准。
176
+ 把用户原始 PRD **原样复制** 到 `.harness/tasks/<task-id>/source/references/`,并写入 `source/source-manifest.json`(含 SHA-256)与 `task.json.referenceDocs`。此步骤不调用模型、不改写内容。随后用 `task source prepare --use-imported-prd --apply` 派生 managed `source/需求.md` / `执行约束.md`;冲突时以 `source/references/*` 为准。
172
177
 
173
178
  `referenceDocs` 是 `{ path, name? }[]` 对象数组,不是路径字符串数组;`import-prd` 会确定性写入正确结构。
174
179
 
@@ -213,22 +218,19 @@ one-shot run evidence 位于 `.harness/runs/{active,completed,failed}/<run-id>/`
213
218
 
214
219
  ### 运行任何 step 前:准备 source materials
215
220
 
216
- `new-task` 之后,先归档原始 PRD,再写派生执行契约:
221
+ `new-task` 之后,先归档原始 PRD,再 `task source prepare` 派生执行契约:
217
222
 
218
223
  ```bash
219
224
  loop-agent import-prd <task-id> --file <path-to-original-prd.md>
220
- # then write derived contract:
221
- mkdir -p <repo-root>/.harness/tasks/<task-id>/source
222
- cat > <repo-root>/.harness/tasks/<task-id>/source/需求.md
225
+ loop-agent task source prepare <task-id> \
226
+ --use-imported-prd \
227
+ --allowed-path "<glob>" \
228
+ --forbidden-path ".harness/**" \
229
+ --verify "typecheck:npm run typecheck" \
230
+ --apply --json
223
231
  ```
224
232
 
225
- `需求.md` 写目标、范围、非目标与验收标准,并映射回 `source/references/*`;不要让 AI 直接改写原始 PRD。
226
-
227
- 若 task 有硬约束(仅允许特定文件、禁止改动),另加:
228
-
229
- ```bash
230
- cat > <repo-root>/.harness/tasks/<task-id>/source/执行约束.md
231
- ```
233
+ 默认无 LLM 写 source;工程边界用 flags 显式给出。不要让 AI 直接改写 `source/references/*`。高级用户仍可手工编辑后 `task contract adopt`。
232
234
 
233
235
  ### Feature-study workflow(参考代码 → 轻量实现)
234
236
 
@@ -54,10 +54,9 @@ Minimum governed path:
54
54
 
55
55
  ```bash
56
56
  loop-agent new-task <task-id> "Task Title" [--repo-root <target-repo>]
57
- # Prefer import-prd for PRD files; plan create for non-trivial work (source-and-plan-practice.md)
58
- # loop-agent import-prd <task-id> --file <path-to-original-prd.md> [--repo-root <target-repo>]
59
- # write derived <target-repo>/.harness/tasks/<task-id>/source/需求.md
60
- # write <target-repo>/.harness/tasks/<task-id>/source/执行约束.md
57
+ # PRD-first: import-prd task source prepare --apply
58
+ loop-agent import-prd <task-id> --file <path-to-original-prd.md> [--repo-root <target-repo>]
59
+ loop-agent task source prepare <task-id> --use-imported-prd --allowed-path "<glob>" --apply --json [--repo-root <target-repo>]
61
60
 
62
61
  loop-agent dag run-task <task-id> \
63
62
  --profile auto \
@@ -67,7 +67,7 @@ loop-agent run-dag --dag <temp-dir>/hybrid-dag.json --init-only --canvas-path <t
67
67
 
68
68
  - **常规 validation**:`dag validate --dag <path>` 做 schema/topology/ranks。JSON 输出含 `governanceProfile`(确定性 `minimal|standard|reviewed|supervised` 推断,含 `process` / `delivery` / `codeChange` signal 与 `reasons`),及 model-matrix drift、governance lint(如 read-only artifact-boundary drift 或 DAG 内 `check-repo.sh` shell env drift)的 warnings。手写临时 DAG spec 执行前用 `dag validate --dag <path> --strict-models`;governance warning 应 fail fast 时加 `--strict-governance`。含 `executor: "cursor"` 的旧 DAG 会在 schema 校验失败;默认生成 DAG 使用 `pi` read-only / Pi write profile / shell。仅当有意在 `.harness/dag-runs/active/` 要 active run snapshot 时用 `run-dag --dry-run`。
69
69
  - **Governance profile 推断与 routing(code vs skill 分工)**:`./src/workflows/dag/governance-profile.ts` 从 DAG 结构与 write scope 做 **硬确定性推断**。JSON 输出 **报告** `process` / `delivery` / `codeChange` signal 与人类可读 `reasons`;`profile` tier(`minimal|standard|reviewed|supervised`)仅由该模块 code rule 选择(如多个 exclusive writer、repair node、review-gate topology、`loop-agent-runtime-paths`、`scripts-ci-harness-paths`、weak post-implementation shell verification、supervised topology)。baseline `forbiddenPaths`(`.harness/**`、`.harness/dag-runs/**`、`artifacts/**`)是默认 governance,**本身不是** process-risk signal。skill prompt 与本 reference **解释** tier 并摘要 profile 选择原因;不替代 code 推断。`dag run-task` 转发 embedded validate step 的同一 candidate `governanceProfile`。`dag run-task --profile auto` 先将 candidate profile 经 `harness.json.workflowPolicy.dag.profileRouting` 映射,再在 candidate delivery signal 含 `loop-agent-runtime-paths`、`scripts-ci-harness-paths` 或 `public-contract-paths` 时应用 M4 `supervised-quality-gate` promotion;`profileRouting.routingReasons` 记录确定性 reason。无 profile `dag run-task <task-id>` 仍为 standard-compatible;显式 `--profile minimal|standard|reviewed|supervised` 与自动 promotion 记录治理强度,已识别的前端业务 workflow 仍使用前端专用模板。高风险 task 应用 `--profile auto` 或显式 `--profile supervised`,而非显式 `--profile reviewed`。
70
- - **Executor model routing**:DAG spec 选 `executor` 与 `complexity`,可通过 `executorModels.pi` 覆盖 model 名;不选 provider。默认 routing:Pi LOW=`gpt-5.3-codex-spark`、MED=`gpt-5.5`、HIGH=`gpt-5.5`。`shell` 不用 model,忽略 `executorModels`。
70
+ - **Executor model routing**:DAG spec 选 `executor` 与 `complexity`,可通过 `executorModels.pi` 覆盖模型。值写成 `provider/model` 时显式选择 Pi provider(只分割第一个 `/`);裸模型名继续走内置映射或默认 `wizard-local`。默认 routing:Pi LOW=`gpt-5.3-codex-spark`、MED=`gpt-5.5`、HIGH=`gpt-5.5`。`shell` 不用 model,忽略 `executorModels`。
71
71
  - **Active visibility**:真实 `run-dag` execution 在 run/node 转换时写 active `state.json`,归档前 core runner 暴露 isolated `DagRunObserver` hook 供 derived view。`.harness/dag-runs/completed/<run-id>/` / `paused/<run-id>/` 仍是 source of truth;observer 输出非 canonical。
72
72
  - **可选 Canvas**:传 `--canvas-path <abs-path>` 或 `--canvas <name>` 输出 derived `.canvas.tsx` live view。省略 flag 行为不变。`--init-only` + Canvas 无需 `CURSOR_API_KEY`。
73
73
 
@@ -232,7 +232,7 @@ review-heavy DAG 中长 shell stdout 可能掩盖 proof 时,用 **evidence-sum
232
232
  | --- | ------- | -------- |
233
233
  | 1 | Topology | 优先 same-rank parallel read-only scout/review;仅 output 真正需要时加 `depends_on` |
234
234
  | 2 | Executor | 每个 task 显式声明 `executor`;`defaults.executor` 是 schema metadata,非 runtime fallback |
235
- | 3 | Model routing | 用 node `complexity` + `executorModels`;Pi MED 与 HIGH 当前都使用 `gpt-5.5`,切换 complexity 不会切换模型;provider 临时不可用时仅使用有记录、有限范围的 worker `--pi-model` smoke override,勿 mutate canonical model matrix |
235
+ | 3 | Model routing | 用 node `complexity` + `executorModels`;需要固定 provider 时写 `provider/model`,裸模型名保持兼容;Pi MED 与 HIGH 默认都使用 `gpt-5.5`,切换 complexity 不会切换默认模型;provider 临时不可用时仅使用有记录、有限范围的 worker `--pi-model` smoke override,勿 mutate canonical model matrix |
236
236
  | 4 | Read-only output | read-only / Pi node 在 **node output** 返回发现;runner 归档于 `.harness/dag-runs/<run-id>/<node-id>/` |
237
237
  | 5 | Root `artifacts/**` | 非 read-only handoff target;持久记录去 `ai_workspace/loop-agent/reports/`、`ai_workspace/loop-agent/progress/`,或 narrow exclusive `writeSet` 写 legacy 摘要并记录迁移计划 |
238
238
  | 6 | DAG Cursor artifacts | 写入 `.harness/dag-runs/<state>/<run-id>/artifacts/<node-id>/`;不得写入 `./artifacts/**` |
@@ -17,6 +17,8 @@
17
17
 
18
18
  Agent DAG node 的模型来自 DAG JSON 中的 `executorModels`,并由 `dag validate --strict-models` 检查 canonical matrix 漂移。若变更模型配置,须同步更新 `harness.json`、相关测试、repo docs 与本 skill。
19
19
 
20
+ `executorModels.pi` 的每档值支持两种形式:裸模型名继续走内置映射或默认 `wizard-local`;`provider/model` 限定引用则显式选择 Pi provider,只在第一个 `/` 处分割并保留模型 ID 的剩余部分。provider 定义与凭证仍归 Pi 的 `~/.pi/agent/models.json` 和环境配置所有。例如不同档可分别写 `wizard-local/glm-5.2` 与 `deepseek/deepseek-v4-flash`。
21
+
20
22
  `pi-prompt` 是独立 one-shot helper,不使用 `harness.json.models` 或 DAG `executorModels`。当前默认是 `wizard-local/glm-5.2`;高复杂度 one-shot 显式传 `--model gpt-5.5`。Agent DAG `pi` executor 的 canonical matrix 保持;读写 profile 共用同一矩阵:
21
23
 
22
24
  ```json
@@ -27,7 +27,7 @@ DAG run、promotion、closeout 和最终验证完成后:
27
27
 
28
28
  ```
29
29
  1. new-task <id>
30
- 2. 有 PRD 文件:import-prd;再写 source/需求.md + source/执行约束.md
30
+ 2. 有 PRD 文件:import-prd → task source prepare --use-imported-prd --apply(默认不手写两 source
31
31
  3. 非微小:plan create(或挂到已有 active plan)
32
32
  4. dag run-task <id> --profile auto --strict-models
33
33
  5. dag validate --dag .harness/tasks/<id>/dag.json --strict-models --strict-governance
@@ -11,7 +11,7 @@ Task 目录布局:`task-workflow.md`。
11
11
  | 层 | 命令 / 路径 | 作用 | 主路径 DAG 是否强制 |
12
12
  | --- | --- | --- | --- |
13
13
  | Source 事实 | `import-prd` → `source/references/*` + `source-manifest.json` | 原始 PRD **不可变**归档 | 否(有 PRD 文件时**强烈推荐**) |
14
- | Source 契约 | 手写 `source/需求.md`、`执行约束.md` + `task.json` 路径边界 | DAG 生成与验收真源 | **是**(至少 `需求.md`) |
14
+ | Source 契约 | `task source prepare --apply` 投影 `source/需求.md`、`执行约束.md` + managed paths | DAG 生成与验收真源 | **是**(至少 `需求.md`;默认不手写) |
15
15
  | Exec-plan | `plan create` / `plan complete` / `plan check` | 仓库级计划索引与交接 | 否(**非微小**推荐) |
16
16
  | DAG 运行时 | `dag run-task` → `dag validate` → `run-dag` | 可执行编排 | **是**(常规实现) |
17
17
 
@@ -47,7 +47,8 @@ Task 目录布局:`task-workflow.md`。
47
47
  ```bash
48
48
  loop-agent new-task <task-id> "简短标题"
49
49
  loop-agent import-prd <task-id> --file <path-to-original-prd.md> [--json]
50
- # 再写派生契约(映射 REQ/AC 或原文锚点)
50
+ loop-agent task source prepare <task-id> --use-imported-prd --allowed-path "<glob>" --apply --json
51
+ # 默认无 LLM 写 source;工程边界用 flags 显式给出
51
52
  # .harness/tasks/<task-id>/source/需求.md
52
53
  # .harness/tasks/<task-id>/source/执行约束.md
53
54
  # 同步 task.json.allowedPaths / forbiddenPaths
@@ -8,7 +8,9 @@
8
8
 
9
9
  ```bash
10
10
  loop-agent new-task <task-id> "Task Title"
11
- # 有原始 PRD:loop-agent import-prd <task-id> --file <prd>
11
+ # 推荐:详细 PRD import prepare(无需手写两 source)
12
+ loop-agent import-prd <task-id> --file <prd>
13
+ loop-agent task source prepare <task-id> --use-imported-prd --allowed-path "<glob>" --apply --json
12
14
  # 非微小:loop-agent plan create <plan-id> "<title>"(可与 task 解耦,见 source-and-plan-practice.md)
13
15
  loop-agent dag run-task <task-id> --profile auto --strict-models
14
16
  loop-agent dag validate --dag .harness/tasks/<task-id>/dag.json --strict-models --strict-governance
@@ -35,10 +37,10 @@ loop-agent run-dag --dag .harness/tasks/<task-id>/dag.json --cwd <repo-root>
35
37
  task.json
36
38
  ```
37
39
 
38
- - 用户原始 PRD 用 `loop-agent import-prd <task-id> --file <prd>` 归档到 `source/references/`,禁止 AI 改写。**有文件就 import**;无独立文件才允许只写 `需求.md`(见 `source-and-plan-practice.md` 决策表)。
39
- - `需求.md` 是派生执行契约:写清目标、验收标准、非目标,并用 REQ/AC 或原文锚点映射回 references
40
- - `执行约束.md` 写清允许文件、禁止改动、硬约束和验证命令。
41
- - 若 `ai_workspace/loop-agent/` 已有权威 plan/spec/PRD,优先 `import-prd` 复制,再在 `需求.md` 引用;避免把长 PRD 直接改写成唯一 source。
40
+ - 用户原始 PRD 用 `loop-agent import-prd <task-id> --file <prd>` 归档到 `source/references/`,禁止 AI 改写。**有文件就 import**。
41
+ - 默认用 `task source prepare --use-imported-prd --apply` 投影 `需求.md` / `执行约束.md` 与 managed paths;**不要**默认让 LLM/主会话手写两 source
42
+ - 无独立 PRD 的微小任务可用 `task source prepare --from-text ... --apply` escape hatch(见 `source-and-plan-practice.md`)。
43
+ - 若 `ai_workspace/loop-agent/` 已有权威 plan/spec/PRD,优先 `import-prd` 复制,再 prepare 派生薄契约;避免把长 PRD 直接改写成唯一 source。
42
44
  - 仓库级 exec-plan(`plan create`)与 harness task **解耦**:非微小实现应有 plan 或复用 active plan;微小任务可不建 plan。
43
45
  - Worker / TaskSpec materialize 路径会把 `source_docs` 复制到 `source/references/`,并在派生 `需求.md` 顶部声明“冲突以 references 为准”;`acceptance_refs` 应展开为短摘要而不只写 ID。
44
46
  - review 节点必须三方对照:`source/references/*`(尤其 requirement/acceptance)、派生 `需求.md`、以及实现/验证证据。