@tea-agent/loop-agent 0.34.0 → 0.34.2

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 (32) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/dist/application/task-lifecycle/advance.js +57 -7
  3. package/dist/application/task-lifecycle/plan-transitions.js +13 -21
  4. package/dist/executors/dag-pi-executor.js +18 -3
  5. package/dist/shared/operator/capabilities.js +26 -2
  6. package/dist/shared/timeout-policy.js +5 -0
  7. package/dist/task/source-prepare/prepare.js +49 -2
  8. package/dist/worker/cli.js +16 -0
  9. package/dist/worker/console/interview/grill-me.js +7 -6
  10. package/dist/worker/console/operator-actions.js +231 -22
  11. package/dist/worker/console/prd-intake-bridge.js +393 -0
  12. package/dist/worker/console/recovery-cta.js +35 -6
  13. package/dist/worker/console/server.js +28 -0
  14. package/dist/worker/console/static/assets/index-BpuHmlSP.js +29 -0
  15. package/dist/worker/console/static/index.html +1 -1
  16. package/dist/worker/console/static-src/app/console-types.js +1 -0
  17. package/dist/worker/console/static-src/app/usePrdImport.js +2 -1
  18. package/dist/worker/console/static-src/app/useRecoveryActions.js +24 -1
  19. package/dist/worker/console/static-src/app/useRecoveryConsole.js +19 -1
  20. package/dist/worker/console/static-src/app/useTaskWizard.js +57 -2
  21. package/dist/worker/observability/read-model.js +3 -0
  22. package/dist/workflows/dag/failure-category.js +3 -0
  23. package/dist/workflows/dag/init-hybrid.js +15 -9
  24. package/dist/workflows/dag/liveness-policy.js +2 -1
  25. package/dist/workflows/dag/node-execution.js +3 -2
  26. package/dist/workflows/dag/retry-policy.js +44 -11
  27. package/dist/workflows/dag/runner.js +6 -0
  28. package/dist/workflows/dag/scheduler.js +49 -1
  29. package/dist/workflows/dag/validate.js +16 -2
  30. package/docs/templates/agent-dag.schema.json +4 -4
  31. package/package.json +1 -1
  32. package/dist/worker/console/static/assets/index-CMHovlqG.js +0 -32
package/CHANGELOG.md CHANGED
@@ -2,6 +2,51 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.34.2] - 2026-08-12
6
+
7
+ ### 重点更新
8
+
9
+ - 修复控制台 DAG 执行在 120 秒后被强制终止导致状态卡死的问题,默认超时现提升至 4 小时
10
+
11
+ ### 改进
12
+
13
+ - DAG 挂钟时间常量统一收敛至 src/shared 共享模块管理,移除了 workflows 中不必要的内核导入
14
+
15
+ ### 修复
16
+
17
+ - 修复控制台任务导航执行 DAG 时因 LoopAgentClient 默认 120 秒超时被强制终止的问题,现已将默认超时提升至 4 小时,并支持通过 --default-timeout-ms 参数覆盖
18
+ - 修正自定义模型合并报告中的文档描述错误
19
+
20
+ ## [0.34.1] - 2026-08-11
21
+
22
+ ### 重点更新
23
+
24
+ - Console PRD Intake Bridge:统一粘贴、单文件与多文件 PRD 导入流程,复用确定性解析与 Semantic Intake,并回写真实 Console Draft
25
+ - 引入工程边界表单,工程路径与验证命令仅接受人工输入,杜绝使用模板内容冒充需求事实
26
+ - DAG 中断恢复增强:支持协作式 abortSignal,未启动节点明确标记为 controller-interrupted,避免粗暴终止
27
+ - Console Recovery 提供直观的 reconcile CTA,按运行状态精准展示恢复入口,破坏性操作需经浏览器 Human Gate 确认
28
+
29
+ ### 新增
30
+
31
+ - Console PRD 导入桥接:支持将粘贴、单文件与多文件导入统一接入 task advance 的确定性解析,并映射产物至 DraftStore
32
+ - Console 工程边界表单:在 PRD 导入时支持可选的 allowed/forbidden/verify 字段,并作为标志传递至 bootstrap intake
33
+ - Console Recovery CTA:基于 effective status 与 liveness 展示 dag reconcile-run(supersede/abandon)入口
34
+ - DAG runner 协作式 abortSignal:主控停止时允许已运行节点按 checkpoint 收尾,未启动节点标记 controller-interrupted
35
+
36
+ ### 改进
37
+
38
+ - dag reconcile-run 增加 --json 与 --action 参数,避免 operation runner 因期待 JSON 失败,并明确拒绝非法 action
39
+ - task advance 生命周期优化:PRD 导入后默认进入仅导入阶段,确保多文件 bootstrap 完成后再执行 Semantic Intake
40
+ - Console Recovery 状态映射细化:将 interrupted/orphaned 等状态映射为 needs-reconcile,suspected-stall 映射为 observe/doctor
41
+ - task advance writeSet gate 输出附确定性 delta,清晰展示边界扩张来源,且不破坏既有 approve 流程
42
+
43
+ ### 修复
44
+
45
+ - 修复追加 PRD 后两阶段 intake 未重新投影契约、Console 可能回读旧 Semantic Intake artifact 的问题
46
+ - 修复 implement-pi 在 provider clean timeout 且明确零写入时直接终止的问题,现对标准 bounded writer 自动重试一次
47
+ - 修复 grill-me 模板被误当作已解析需求的问题
48
+ - 修复窄屏(<1100px)下 CHANGELOG 文档中尖括号导致的 MDX JSX 解析错误
49
+
5
50
  ## [0.34.0] - 2026-08-11
6
51
 
7
52
  ### 重点更新
@@ -42,7 +42,8 @@ function hasEngineeringBoundary(input) {
42
42
  }
43
43
  /** Inputs that must produce a new managed-contract revision when explicit. */
44
44
  function hasContractChangingInput(input) {
45
- return Boolean(hasEngineeringBoundary(input) ||
45
+ return Boolean((input.prdPaths?.length ?? 0) > 0 ||
46
+ hasEngineeringBoundary(input) ||
46
47
  input.title ||
47
48
  input.taskKind ||
48
49
  input.featureId);
@@ -339,7 +340,7 @@ export async function advanceTaskLifecycle(input) {
339
340
  verifyCommands: applied.verifyCommands ?? input.verifyCommands,
340
341
  };
341
342
  const hasBoundary = hasEngineeringBoundary(effectiveInput);
342
- let refreshManagedContractThisCall = Boolean(hasContractChangingInput(effectiveInput) &&
343
+ let refreshManagedContractThisCall = Boolean((hasContractChangingInput(effectiveInput) || record.pendingPrdRefresh) &&
343
344
  (snapshot.contract?.effectiveStatus === "managed" ||
344
345
  Boolean(snapshot.contract?.revision && snapshot.contract.revision > 0)));
345
346
  if (applied.accepted) {
@@ -515,6 +516,7 @@ export async function advanceTaskLifecycle(input) {
515
516
  continue;
516
517
  }
517
518
  if (action.type === "import-prd") {
519
+ const importedMaterializedPaths = [];
518
520
  for (const prd of input.prdPaths ?? []) {
519
521
  let markdown;
520
522
  try {
@@ -528,7 +530,7 @@ export async function advanceTaskLifecycle(input) {
528
530
  explicitRole: prd.role,
529
531
  markdown,
530
532
  });
531
- await importPrdDocument({
533
+ const imported = await importPrdDocument({
532
534
  repoRoot: input.repoRoot,
533
535
  taskId: input.taskId,
534
536
  filePath: prd.filePath,
@@ -536,15 +538,48 @@ export async function advanceTaskLifecycle(input) {
536
538
  // Prefer basename-aligned names for multi-file product suites.
537
539
  name: path.basename(prd.filePath).replace(/\.(md|markdown|txt)$/iu, "") || undefined,
538
540
  });
541
+ importedMaterializedPaths.push(imported.materializedRelativePath);
539
542
  }
540
543
  // Latch even when prdPaths is empty so a mis-planned import cannot loop.
541
544
  importedPrdThisCall = true;
542
545
  if ((input.prdPaths?.length ?? 0) > 0) {
543
- record = appendTransition(record, "prd-imported");
546
+ record = {
547
+ ...appendTransition(record, "prd-imported"),
548
+ pendingPrdRefresh: true,
549
+ pendingPrdRefreshPaths: Array.from(new Set([
550
+ ...(record.pendingPrdRefreshPaths ?? []),
551
+ ...importedMaterializedPaths,
552
+ ])),
553
+ };
544
554
  completed.push("prd-imported");
545
555
  await saveLifecycleRecord(input.repoRoot, record);
546
556
  }
547
557
  snapshot = await observeTaskLifecycle(input.repoRoot, input.taskId);
558
+ // Two-phase Console bridge: when this call only had --prd and no
559
+ // engineering boundary, stop after archive. Re-plan would otherwise
560
+ // immediately prepare (hasPrdInput latched false + alreadyImported)
561
+ // and burn one-shot Semantic Intake before multi-file imports finish.
562
+ if (!hasBoundary && (input.prdPaths?.length ?? 0) > 0) {
563
+ return {
564
+ taskId: input.taskId,
565
+ lifecycleState: "intake-incomplete",
566
+ completedTransitions: uniqueTransitions(record.completedTransitions, completed),
567
+ gate: null,
568
+ blockers: [],
569
+ warnings: [
570
+ ...warnings,
571
+ {
572
+ code: "BOUNDARY_REQUIRED",
573
+ message: "PRD imported; re-run task advance (optionally with --allowed-path / --verify) to prepare contract / semantic intake",
574
+ },
575
+ ],
576
+ next: {
577
+ kind: "advance",
578
+ command: `loop-agent task advance ${input.taskId} --json`,
579
+ },
580
+ recommendations,
581
+ };
582
+ }
548
583
  continue;
549
584
  }
550
585
  if (action.type === "prepare-contract") {
@@ -580,6 +615,8 @@ export async function advanceTaskLifecycle(input) {
580
615
  : {
581
616
  kind: "facts",
582
617
  useImportedPrd: hasPrd || (snapshot.source?.importedPrdCount ?? 0) > 0,
618
+ replaceManagedRequirement: record.pendingPrdRefresh === true,
619
+ importedPrdPaths: record.pendingPrdRefreshPaths,
583
620
  text: input.fromText,
584
621
  textLabel: input.fromText ? "from-text" : undefined,
585
622
  },
@@ -675,7 +712,11 @@ export async function advanceTaskLifecycle(input) {
675
712
  refreshManagedContractThisCall = false;
676
713
  regenerateDagThisCall = true;
677
714
  }
678
- record = appendTransition(record, "contract-managed");
715
+ record = {
716
+ ...appendTransition(record, "contract-managed"),
717
+ pendingPrdRefresh: false,
718
+ pendingPrdRefreshPaths: [],
719
+ };
679
720
  completed.push("contract-managed");
680
721
  await saveLifecycleRecord(input.repoRoot, record);
681
722
  snapshot = await observeTaskLifecycle(input.repoRoot, input.taskId);
@@ -706,7 +747,12 @@ export async function advanceTaskLifecycle(input) {
706
747
  const softIntakeAfter = Boolean(prepareResult.error?.details?.softIntake) ||
707
748
  (gapCodesAfter.length > 0 &&
708
749
  gapCodesAfter.every((code) => isIntakeSoftGapCode(code)));
709
- if (softIntakeAfter && completed.includes("prd-imported")) {
750
+ // Soft-stop when PRD is on disk (this call or prior import).
751
+ // Console two-phase bootstrap: import first, then advance intake
752
+ // without re-passing --prd — still must soft-stop, not hard-fail.
753
+ const hasImportedPrdOnDisk = completed.includes("prd-imported") ||
754
+ (snapshot.source?.importedPrdCount ?? 0) > 0;
755
+ if (softIntakeAfter && hasImportedPrdOnDisk) {
710
756
  warnings.push({
711
757
  code: "INTAKE_INCOMPLETE",
712
758
  message: prepareResult.error?.message ??
@@ -750,7 +796,11 @@ export async function advanceTaskLifecycle(input) {
750
796
  refreshManagedContractThisCall = false;
751
797
  regenerateDagThisCall = true;
752
798
  }
753
- record = appendTransition(record, "contract-managed");
799
+ record = {
800
+ ...appendTransition(record, "contract-managed"),
801
+ pendingPrdRefresh: false,
802
+ pendingPrdRefreshPaths: [],
803
+ };
754
804
  completed.push("contract-managed");
755
805
  await saveLifecycleRecord(input.repoRoot, record);
756
806
  snapshot = await observeTaskLifecycle(input.repoRoot, input.taskId);
@@ -62,17 +62,21 @@ export function planTransitions(input) {
62
62
  expectedTransitions.push("contract-managed");
63
63
  }
64
64
  else if (!sourceReady || !contractManaged) {
65
- if ((shouldImportPrd || alreadyImportedPrd) &&
66
- !input.hasEngineeringBoundary) {
67
- // Console bootstrap / import-only: archive PRD(s), do not prepare contract yet.
68
- if (shouldImportPrd) {
69
- actions.push({ type: "import-prd" });
70
- expectedTransitions.push("prd-imported");
71
- }
65
+ // Console PRD Intake Bridge (2026-08-11), two-phase:
66
+ // 1) hasPrdInput without engineering boundary → import only (archive
67
+ // references). Do NOT prepare yet so multi-file bootstrap can finish
68
+ // all imports before the one-shot Semantic Intake burns.
69
+ // 2) alreadyImportedPrd without new --prd (and/or with boundary)
70
+ // prepare-contract so deterministic parse + Semantic Intake run.
71
+ // prepare soft-stops on EMPTY_ALLOWED_PATHS etc. and never opens the
72
+ // writeSet gate without a complete managed contract.
73
+ if (shouldImportPrd && !input.hasEngineeringBoundary) {
74
+ actions.push({ type: "import-prd" });
75
+ expectedTransitions.push("prd-imported");
72
76
  actions.push({ type: "stop", reason: "blocker" });
73
77
  blockers.push({
74
78
  code: "BOUNDARY_REQUIRED",
75
- message: "PRD imported; provide --allowed-path / --verify (or --from-text with boundaries) to prepare contract and open writeSet gate",
79
+ message: "PRD imported; re-run task advance (optionally with --allowed-path / --verify) to prepare contract / semantic intake",
76
80
  });
77
81
  return {
78
82
  actions,
@@ -80,7 +84,7 @@ export function planTransitions(input) {
80
84
  blockers,
81
85
  next: {
82
86
  kind: "advance",
83
- command: `loop-agent task advance ${snapshot.taskId} --allowed-path <glob> --verify <label:command> --json`,
87
+ command: `loop-agent task advance ${snapshot.taskId} --json`,
84
88
  },
85
89
  expectedTransitions,
86
90
  };
@@ -218,18 +222,6 @@ export function planTransitions(input) {
218
222
  expectedTransitions,
219
223
  };
220
224
  }
221
- function stopWith(actions, expectedTransitions, blockers, snapshot, reason) {
222
- return {
223
- actions: [...actions, { type: "stop", reason }],
224
- gate: snapshot.gate ?? null,
225
- blockers,
226
- next: {
227
- kind: "advance",
228
- command: `loop-agent task advance ${snapshot.taskId} --json`,
229
- },
230
- expectedTransitions,
231
- };
232
- }
233
225
  function deriveNext(snapshot, gate, approveGate) {
234
226
  if (gate && !approveGate) {
235
227
  return { kind: "approve-gate", command: gate.approveCommand };
@@ -8,7 +8,7 @@ import { cleanupPlaywrightCliDefaultSession, createPlaywrightCliTool, PI_COMMAND
8
8
  import { dagCommandPolicyAllows, resolveDagCommandPolicy, } from "../workflows/dag/types.js";
9
9
  import { redactPromptForLog, truncateOutput, } from "../shared/output-truncation.js";
10
10
  import { GitStatusUnavailableError, pathsChangedDuringRun, readGitStatusPorcelain, recoverRootNulArtifact, snapshotGitStatusPathFingerprints, snapshotGitStatusPorcelain, validateShellWriteGuard, } from "./shell-write-guard.js";
11
- import { isWriterEmptyDiffRetryCandidate, INCOMPLETE_WRITE_SET_RETRY_CATEGORY, WRITER_EMPTY_DIFF_RETRY_CATEGORY, } from "../workflows/dag/retry-policy.js";
11
+ import { isWriterEmptyDiffRetryCandidate, isWriterTransportRetryCandidate, INCOMPLETE_WRITE_SET_RETRY_CATEGORY, WRITER_CLEAN_TIMEOUT_RETRY_CATEGORY, WRITER_EMPTY_DIFF_RETRY_CATEGORY, } from "../workflows/dag/retry-policy.js";
12
12
  import { assessBackendTestMdPlanCompleteness, assessBackendTestMdWriterCompleteness, assessBackendTestPytestPlanCompleteness, assessBackendTestPytestWriterCompleteness, assessBackendTestShardChildCompleteness, backendTestWriterProgressRoleForTask, classifyBackendTestWriterCompletenessFailure, isBackendTestCompletenessRetryCandidate, isBackendTestMdPlanTask, isBackendTestPytestPlanTask, isBackendTestShardChildTask, writeBackendTestWriterProgressArtifacts, } from "../workflows/dag/backend-test-writer-completeness.js";
13
13
  import { redactSecrets, truncateUtf8Preview } from "../shared/preview.js";
14
14
  /**
@@ -831,6 +831,18 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
831
831
  if (meta.writeGuardAttribution === "best-effort") {
832
832
  stderrParts.push("write guard note: concurrent rank writers use best-effort per-node attribution; keep same-rank writeSet entries disjoint");
833
833
  }
834
+ // Pure provider stall/timeout with zero workspace side effects is eligible
835
+ // for a single writer transport retry (implement-pi flake resilience).
836
+ // Partial writes, tool calls, or write-guard failures keep raw `timeout`.
837
+ const writerCleanTimeout = !mapped.ok &&
838
+ mapped.failureCategory === "timeout" &&
839
+ writeGuardOk &&
840
+ !completenessFailure &&
841
+ !writerThinkingExhausted &&
842
+ isWriterTransportRetryCandidate(input.task) &&
843
+ changeManifestChangedFiles !== undefined &&
844
+ changeManifestChangedFiles.length === 0 &&
845
+ readWriterThinkingExhaustionEvidence(result).writeToolCallCount === 0;
834
846
  return {
835
847
  ...mapped,
836
848
  ok: false,
@@ -851,7 +863,8 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
851
863
  // the continuation is retryable and carries the embedded target paths.
852
864
  // Provider/transport failures (quota, auth, network, timeout,
853
865
  // rate-limit, unavailable) keep their original category so a provider
854
- // outage is never masked as a recoverable local write problem.
866
+ // outage is never masked as a recoverable local write problem — except
867
+ // clean timeouts with zero attributed writes, remapped below.
855
868
  writeGuardOk &&
856
869
  completenessFailure?.failureCategory ===
857
870
  INCOMPLETE_WRITE_SET_RETRY_CATEGORY &&
@@ -867,7 +880,9 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
867
880
  // upgrade to incomplete-write-set above.
868
881
  writerThinkingExhausted
869
882
  ? WRITER_THINKING_EXHAUSTED_CATEGORY
870
- : mapped.failureCategory,
883
+ : writerCleanTimeout
884
+ ? WRITER_CLEAN_TIMEOUT_RETRY_CATEGORY
885
+ : mapped.failureCategory,
871
886
  durationMs: mapped.durationMs || Date.now() - started,
872
887
  };
873
888
  }
@@ -586,13 +586,13 @@ export function buildOperatorCapabilitiesDocument() {
586
586
  },
587
587
  {
588
588
  action: "importPrd",
589
- cli: "loop-agent task advance <id> --prd <server-staged-path> --json",
589
+ cli: "loop-agent task advance <id> --prd <server-staged-path> --json (+ intake bridge)",
590
590
  kind: "mutation",
591
591
  inputSchemaVersion: 1,
592
592
  resultSchemaVersion: 1,
593
593
  envelopeSchemaVersion: 1,
594
594
  requiredErrorCodes: [...COMMON_MUTATION_ERRORS, "PATH_OUTSIDE_REPO"],
595
- description: "Archive immutable PRD via task advance (server-staged file).",
595
+ description: "Archive PRD via task advance, run intake bridge (deterministic/semantic), rewrite Console draft.",
596
596
  inputParams: [
597
597
  {
598
598
  name: "taskId",
@@ -606,6 +606,30 @@ export function buildOperatorCapabilitiesDocument() {
606
606
  required: true,
607
607
  description: "PRD markdown content",
608
608
  },
609
+ {
610
+ name: "taskKind",
611
+ type: "string",
612
+ required: false,
613
+ description: "workflow kind (human-selected)",
614
+ },
615
+ {
616
+ name: "allowedPaths",
617
+ type: "array",
618
+ required: false,
619
+ description: "optional engineering allowedPaths (human-confirmed)",
620
+ },
621
+ {
622
+ name: "forbiddenPaths",
623
+ type: "array",
624
+ required: false,
625
+ description: "optional engineering forbiddenPaths",
626
+ },
627
+ {
628
+ name: "verifyCommands",
629
+ type: "array",
630
+ required: false,
631
+ description: "optional verify commands (label:command or shell)",
632
+ },
609
633
  ],
610
634
  modelCallable: "always",
611
635
  humanConfirmation: "none",
@@ -1,5 +1,10 @@
1
1
  import { DEFAULT_TIMEOUT_MS, MAX_ALLOWED_TIMEOUT_MS } from "../executors/pi-executor.js";
2
2
  export { MAX_ALLOWED_TIMEOUT_MS };
3
+ /**
4
+ * DAG runner 绝对最大墙钟(4h)。不可被空心跳续期;
5
+ * 是 DEFAULT_LIVENESS_POLICY 与 Console LoopAgentClient 默认超时的单一真源。
6
+ */
7
+ export const DAG_ABSOLUTE_MAX_WALL_CLOCK_MS = 14_400_000;
3
8
  /**
4
9
  * Clamp timeout values for Pi steps. Undefined or non-positive values fall back to
5
10
  * DEFAULT_TIMEOUT_MS so task-level invalid values do not disable Pi execution timeouts.
@@ -331,7 +331,19 @@ export async function prepareTaskSource(input) {
331
331
  constraintsPath: contractPaths.constraintsPath,
332
332
  });
333
333
  if (projection.ok) {
334
- baseDraft = projection.draft;
334
+ baseDraft = input.intent.replaceManagedRequirement
335
+ ? {
336
+ ...projection.draft,
337
+ requirement: {
338
+ objective: "",
339
+ scope: [],
340
+ nonGoals: [],
341
+ acceptanceCriteria: [],
342
+ },
343
+ openQuestions: [],
344
+ assumptions: [],
345
+ }
346
+ : projection.draft;
335
347
  }
336
348
  else {
337
349
  managedProjectionError = projection.message;
@@ -396,7 +408,30 @@ export async function prepareTaskSource(input) {
396
408
  taskId,
397
409
  requireParseableRequirement: false,
398
410
  });
399
- hasImportedPrd = true;
411
+ const requestedPaths = new Set((input.intent.importedPrdPaths ?? [])
412
+ .map((value) => value.replace(/\\/g, "/").trim())
413
+ .filter(Boolean));
414
+ if (requestedPaths.size > 0) {
415
+ const selectedDocuments = sourceIntegrity.documents.filter((document) => requestedPaths.has(document.materializedPath.replace(/\\/g, "/")));
416
+ const selectedPaths = new Set(selectedDocuments.map((document) => document.materializedPath.replace(/\\/g, "/")));
417
+ for (const requestedPath of requestedPaths) {
418
+ if (selectedPaths.has(requestedPath))
419
+ continue;
420
+ sourceIntegrity.issues.push({
421
+ code: "REFERENCE_NOT_IN_MANIFEST",
422
+ level: "blocking",
423
+ message: `current import batch reference not found in source-manifest: ${requestedPath}`,
424
+ path: requestedPath,
425
+ });
426
+ sourceIntegrity.ok = false;
427
+ }
428
+ sourceIntegrity = {
429
+ ...sourceIntegrity,
430
+ documents: selectedDocuments,
431
+ parseableDocuments: selectedDocuments.filter((document) => document.parseable),
432
+ };
433
+ }
434
+ hasImportedPrd = sourceIntegrity.documents.length > 0;
400
435
  if (sourceIntegrity.ok) {
401
436
  references = draftReferencesFromManifest(sourceIntegrity.documents);
402
437
  let acStart = 1;
@@ -517,6 +552,7 @@ export async function prepareTaskSource(input) {
517
552
  "PRODUCT_CLARIFICATION_NOT_EXECUTABLE",
518
553
  "PRODUCT_REQUIREMENT_PENDING",
519
554
  ].includes(gap.code));
555
+ const hasEmptyAcceptance = gaps.some((gap) => gap.code === "EMPTY_ACCEPTANCE");
520
556
  for (const message of validation.errors) {
521
557
  const isAcceptanceSchema = /acceptance criterion|acceptanceCriteria/i.test(message);
522
558
  // Prefer product-artifact diagnostics over generic schema AC errors.
@@ -528,6 +564,17 @@ export async function prepareTaskSource(input) {
528
564
  });
529
565
  continue;
530
566
  }
567
+ // Soft-intake path: completeness already has EMPTY_ACCEPTANCE (soft).
568
+ // Do not add a hard DRAFT_VALIDATION twin that flips softIntake=false
569
+ // and blocks one-shot Semantic Intake for free-form PRDs.
570
+ if (hasEmptyAcceptance && isAcceptanceSchema) {
571
+ gaps.push({
572
+ code: "DRAFT_VALIDATION",
573
+ level: "warning",
574
+ message: `${message} (soft intake; EMPTY_ACCEPTANCE primary)`,
575
+ });
576
+ continue;
577
+ }
531
578
  gaps.push({
532
579
  code: "DRAFT_VALIDATION",
533
580
  level: "blocking",
@@ -720,6 +720,7 @@ export function buildAgentWorkerProgram() {
720
720
  .option("--host <host>", "Bind host (default 127.0.0.1; use 0.0.0.0 for LAN, no network auth)", "127.0.0.1")
721
721
  .option("--debug", "Log request source/method/path/status to stderr")
722
722
  .option("--app-data <path>", "Override Console application-data root (Draft/Operation/confirmation)")
723
+ .option("--default-timeout-ms <ms>", "LoopAgentClient hard timeout for Console operations (ms; default 14400000 / 4h; 0 disables)")
723
724
  .action(async (options) => {
724
725
  await runConsoleServe({
725
726
  repoRoot: path.resolve(options.repo),
@@ -729,6 +730,7 @@ export function buildAgentWorkerProgram() {
729
730
  appDataRoot: options.appData
730
731
  ? path.resolve(options.appData)
731
732
  : undefined,
733
+ defaultTimeoutMsRaw: options.defaultTimeoutMs,
732
734
  });
733
735
  });
734
736
  consoleCmd
@@ -738,6 +740,7 @@ export function buildAgentWorkerProgram() {
738
740
  .option("--host <host>", "Bind host (default 127.0.0.1; use 0.0.0.0 for LAN, no network auth)", "127.0.0.1")
739
741
  .option("--debug", "Log request source/method/path/status to stderr")
740
742
  .option("--app-data <path>", "Override Console application-data root (Draft/Operation/confirmation)")
743
+ .option("--default-timeout-ms <ms>", "LoopAgentClient hard timeout for Console operations (ms; default 14400000 / 4h; 0 disables)")
741
744
  .description("Start Loop Operator Console HTTP server (compat entry; equivalent to bare `agent-worker console`)")
742
745
  .action(async (options) => {
743
746
  await runConsoleServe({
@@ -748,6 +751,7 @@ export function buildAgentWorkerProgram() {
748
751
  appDataRoot: options.appData
749
752
  ? path.resolve(options.appData)
750
753
  : undefined,
754
+ defaultTimeoutMsRaw: options.defaultTimeoutMs,
751
755
  });
752
756
  });
753
757
  consoleCmd
@@ -835,6 +839,17 @@ export async function main(argv = process.argv) {
835
839
  async function readTaskYaml(taskSpecPath) {
836
840
  return YAML.parse(await readFile(taskSpecPath, "utf-8"));
837
841
  }
842
+ /** Parse optional `--default-timeout-ms`; undefined keeps Console default. */
843
+ function parseOptionalTimeoutMs(raw) {
844
+ if (raw === undefined || raw.trim() === "") {
845
+ return undefined;
846
+ }
847
+ const value = Number(raw);
848
+ if (!Number.isFinite(value) || value < 0 || !Number.isInteger(value)) {
849
+ throw new Error(`--default-timeout-ms must be a non-negative integer (ms); got ${JSON.stringify(raw)}`);
850
+ }
851
+ return value;
852
+ }
838
853
  /** Shared console serve entry used by both bare `console` and `console serve`. */
839
854
  async function runConsoleServe(options) {
840
855
  const { createConsoleServer } = await import("./console/server.js");
@@ -845,6 +860,7 @@ async function runConsoleServe(options) {
845
860
  port: options.port,
846
861
  debug: options.debug === true,
847
862
  appDataRoot: options.appDataRoot,
863
+ defaultTimeoutMs: parseOptionalTimeoutMs(options.defaultTimeoutMsRaw),
848
864
  });
849
865
  process.stdout.write(`${server.url}\n`);
850
866
  await new Promise((resolve) => {
@@ -82,7 +82,8 @@ export function buildQuestionForGap(gapId, draft, context = {}) {
82
82
  id: `q-${gapId}`,
83
83
  gapId,
84
84
  text: "本轮允许改动的范围(scope)是哪些路径/模块?用逗号或换行列出。",
85
- recommendation: "src/worker/console/**",
85
+ // No product-template default — must come from PRD intake or human override.
86
+ recommendation: "(请按 PRD / 仓库结构填写;勿套用 Console 模板路径)",
86
87
  affectsFields: ["requirement.scope"],
87
88
  risk: "high",
88
89
  },
@@ -90,7 +91,7 @@ export function buildQuestionForGap(gapId, draft, context = {}) {
90
91
  id: `q-${gapId}`,
91
92
  gapId,
92
93
  text: "明确非目标(non-goals)是什么?本轮刻意不做哪些事?",
93
- recommendation: "Phase 4 General Operator Chat;合并 Console/Observe",
94
+ recommendation: "(本轮明确不做的事项;可写「暂无」)",
94
95
  affectsFields: ["requirement.nonGoals"],
95
96
  risk: "medium",
96
97
  },
@@ -98,7 +99,7 @@ export function buildQuestionForGap(gapId, draft, context = {}) {
98
99
  id: `q-${gapId}`,
99
100
  gapId,
100
101
  text: "验收标准有哪些?可用分号或换行列出(将编号为 AC-1…)。",
101
- recommendation: "Happy Path 可走通 Interview→validate→apply;无 Cancel;无直接改代码 CTA",
102
+ recommendation: "(请写可观测验收标准;勿套用产品模板 AC)",
102
103
  affectsFields: ["requirement.acceptanceCriteria"],
103
104
  risk: "high",
104
105
  },
@@ -114,7 +115,7 @@ export function buildQuestionForGap(gapId, draft, context = {}) {
114
115
  id: `q-${gapId}`,
115
116
  gapId,
116
117
  text: "结构化 allowedPaths(写入白名单)?逗号/换行分隔 glob。",
117
- recommendation: "src/worker/console/**, test/worker/console/**",
118
+ recommendation: "(工程边界须人类确认;示例:src/**, test/**)",
118
119
  affectsFields: ["constraints.allowedPaths"],
119
120
  risk: "high",
120
121
  },
@@ -122,7 +123,7 @@ export function buildQuestionForGap(gapId, draft, context = {}) {
122
123
  id: `q-${gapId}`,
123
124
  gapId,
124
125
  text: "forbiddenPaths(禁止写入)?逗号/换行分隔。",
125
- recommendation: "src/cli/**, bin/**, package.json",
126
+ recommendation: ".harness/**, node_modules/**, package.json",
126
127
  affectsFields: ["constraints.forbiddenPaths"],
127
128
  risk: "high",
128
129
  },
@@ -130,7 +131,7 @@ export function buildQuestionForGap(gapId, draft, context = {}) {
130
131
  id: `q-${gapId}`,
131
132
  gapId,
132
133
  text: "验证命令(verifyCommands)?可给一条或多条 shell(换行分隔)。",
133
- recommendation: "npx vitest run test/worker/console",
134
+ recommendation: "npm run typecheck",
134
135
  affectsFields: ["verification.commands"],
135
136
  risk: "medium",
136
137
  },