@tea-agent/loop-agent 0.28.11 → 0.28.12

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.
@@ -351,6 +351,19 @@ export function extractFrontendImplementationJson(text) {
351
351
  return JSON.parse(source);
352
352
  }
353
353
  catch (error) {
354
+ // Apply only deterministic, structure-preserving repairs before the
355
+ // quote repair below. These are common JSONC/model formatting defects,
356
+ // not a general parser relaxation.
357
+ const withoutComments = source
358
+ .replace(/\/\*[\s\S]*?\*\//g, "")
359
+ .replace(/^\s*\/\/.*$/gm, "");
360
+ const withoutTrailingCommas = withoutComments.replace(/,\s*([}\]])/g, "$1");
361
+ try {
362
+ return JSON.parse(withoutTrailingCommas);
363
+ }
364
+ catch {
365
+ // Continue with the narrower embedded-quote repair.
366
+ }
354
367
  // Models sometimes put ordinary ASCII quotes inside a JSON string
355
368
  // (for example: `reason: "支持..."`). Repair only quotes that are
356
369
  // clearly not structural: a closing quote is followed by JSON
@@ -503,6 +516,51 @@ function asStringArray(value) {
503
516
  .map((item) => asString(item))
504
517
  .filter((item) => item.length > 0);
505
518
  }
519
+ function normalizeFrontendRoute(value) {
520
+ const route = asString(value).replaceAll("\\", "/").replace(/^\/+/, "");
521
+ if (!route || route === "." || route.split("/").includes(".."))
522
+ return null;
523
+ return `/${route}`;
524
+ }
525
+ function normalizeFrontendContractOptionalFields(value) {
526
+ const record = asRecord(value);
527
+ if (!record)
528
+ return value;
529
+ const targets = asRecord(record.targets);
530
+ const mockApi = asRecord(record.mockApi);
531
+ return {
532
+ ...record,
533
+ ...(targets
534
+ ? {
535
+ targets: {
536
+ ...targets,
537
+ routes: Array.isArray(targets.routes)
538
+ ? targets.routes.map(normalizeFrontendRoute).filter((route) => Boolean(route))
539
+ : targets.routes,
540
+ },
541
+ }
542
+ : {}),
543
+ ...(mockApi
544
+ ? {
545
+ mockApi: {
546
+ ...mockApi,
547
+ endpoints: Array.isArray(mockApi.endpoints)
548
+ ? mockApi.endpoints.map((item) => {
549
+ const endpoint = asRecord(item);
550
+ if (!endpoint)
551
+ return item;
552
+ return {
553
+ ...endpoint,
554
+ fixture: asString(endpoint.fixture) || undefined,
555
+ consumer: asString(endpoint.consumer) || undefined,
556
+ };
557
+ })
558
+ : mockApi.endpoints,
559
+ },
560
+ }
561
+ : {}),
562
+ };
563
+ }
506
564
  function isRequirementId(value) {
507
565
  return REQUIREMENT_ID_PATTERN.test(value);
508
566
  }
@@ -570,6 +628,112 @@ function canonicalizeVerificationTargetAliases(value) {
570
628
  : record.requirements;
571
629
  return { ...record, requirements };
572
630
  }
631
+ /**
632
+ * Model-written evidence gaps are hypotheses, not authoritative coverage
633
+ * facts. If the plan contains a real verification target that explicitly
634
+ * names a requirement, the executor can prove that the requirement has a
635
+ * verification path even when the model forgot to wire the target into the
636
+ * requirement entry or conservatively marked its gap as blocking.
637
+ *
638
+ * Keep gaps blocking when that proof cannot be derived. This is deliberately
639
+ * narrow: it uses only sourceBinding requirement IDs and structurally present
640
+ * verification targets, and never invents a command, file, or target.
641
+ */
642
+ function deriveFrontendVerificationCoverage(value, canonicalBinding) {
643
+ const record = asRecord(value);
644
+ if (!record)
645
+ return value;
646
+ const rawTargets = Array.isArray(record.verificationTargets)
647
+ ? record.verificationTargets
648
+ : [];
649
+ const targetIds = new Set(rawTargets
650
+ .map((item) => asRecord(item))
651
+ .filter((item) => Boolean(item))
652
+ .map((item) => asString(item.id))
653
+ .filter(Boolean));
654
+ const targetIdsByRequirement = new Map();
655
+ for (const item of rawTargets) {
656
+ const target = asRecord(item);
657
+ if (!target)
658
+ continue;
659
+ const targetId = asString(target.id);
660
+ const commandLabel = asString(target.commandLabel) || asString(target.command);
661
+ const file = asString(target.file);
662
+ // A target is usable evidence only when it has an identity, command and
663
+ // file. The strict schema will validate the final shape afterwards.
664
+ if (!targetId || !commandLabel || !file)
665
+ continue;
666
+ const targetRequirementIds = asStringArray(target.requirementIds);
667
+ const inferredRequirementIds = targetRequirementIds.length > 0
668
+ ? targetRequirementIds
669
+ : rawTargets.length === 1
670
+ ? canonicalBinding.requirementIds
671
+ : [];
672
+ for (const requirementId of inferredRequirementIds) {
673
+ const canonicalId = canonicalizeRequirementId(requirementId);
674
+ if (!canonicalBinding.requirementIds.includes(canonicalId))
675
+ continue;
676
+ const ids = targetIdsByRequirement.get(canonicalId) ?? [];
677
+ if (!ids.includes(targetId))
678
+ ids.push(targetId);
679
+ targetIdsByRequirement.set(canonicalId, ids);
680
+ }
681
+ }
682
+ const provenRequirementIds = new Set(targetIdsByRequirement.keys());
683
+ const requirements = Array.isArray(record.requirements)
684
+ ? record.requirements.map((item) => {
685
+ const requirement = asRecord(item);
686
+ if (!requirement)
687
+ return item;
688
+ const requirementId = canonicalizeRequirementId(asString(requirement.id));
689
+ const inferredTargetIds = targetIdsByRequirement.get(requirementId) ?? [];
690
+ const existingTargetIds = asStringArray(requirement.verificationTargetIds)
691
+ .filter((id) => targetIds.has(id));
692
+ const verificationTargetIds = [...new Set([
693
+ ...existingTargetIds,
694
+ ...inferredTargetIds,
695
+ ])];
696
+ const evidenceGap = asRecord(requirement.evidenceGap);
697
+ const hasProof = provenRequirementIds.has(requirementId);
698
+ return {
699
+ ...requirement,
700
+ ...(verificationTargetIds.length > 0 ? { verificationTargetIds } : {}),
701
+ ...(hasProof
702
+ ? (evidenceGap ? { evidenceGap: { ...evidenceGap, blocking: false } } : {})
703
+ : {
704
+ evidenceGap: {
705
+ requirementId,
706
+ description: `No executable verification target can be derived for ${requirementId}`,
707
+ blocking: true,
708
+ },
709
+ }),
710
+ };
711
+ })
712
+ : record.requirements;
713
+ const modelEvidenceGaps = Array.isArray(record.evidenceGaps)
714
+ ? record.evidenceGaps.map((item) => {
715
+ const gap = asRecord(item);
716
+ if (!gap)
717
+ return item;
718
+ const requirementId = canonicalizeRequirementId(asString(gap.requirementId));
719
+ // Model gaps are advisory. Blocking status is reconstructed below
720
+ // from the source binding and executable verification targets.
721
+ return { ...gap, blocking: false };
722
+ })
723
+ : [];
724
+ const derivedBlockingGaps = canonicalBinding.requirementIds
725
+ .filter((requirementId) => !provenRequirementIds.has(requirementId))
726
+ .map((requirementId) => ({
727
+ requirementId,
728
+ description: `No executable verification target can be derived for ${requirementId}`,
729
+ blocking: true,
730
+ }));
731
+ return {
732
+ ...record,
733
+ requirements,
734
+ evidenceGaps: [...modelEvidenceGaps, ...derivedBlockingGaps],
735
+ };
736
+ }
573
737
  function assertFrontendContractPathsSafe(value) {
574
738
  const record = asRecord(value);
575
739
  if (!record)
@@ -700,12 +864,13 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
700
864
  : rawRecord.uiStates,
701
865
  }
702
866
  : value;
867
+ const normalizedOptionalFields = normalizeFrontendContractOptionalFields(normalizedValue);
703
868
  // A payload can have the strict top-level shape while still containing
704
869
  // empty requirement coverage arrays. Do not trust shape alone: route such
705
870
  // payloads through the compatibility normalizer so targets and verification
706
871
  // references are deterministically filled from the contract context.
707
- if (looksLikeStrictFrontendContract(normalizedValue)) {
708
- const strictRecord = asRecord(normalizedValue);
872
+ if (looksLikeStrictFrontendContract(normalizedOptionalFields)) {
873
+ const strictRecord = asRecord(normalizedOptionalFields);
709
874
  const strictMockApi = asRecord(strictRecord?.mockApi);
710
875
  if (strictMockApi &&
711
876
  typeof strictMockApi.strategy === "string" &&
@@ -730,10 +895,28 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
730
895
  : verificationIds,
731
896
  };
732
897
  });
733
- return { ...strictRecord, requirements };
898
+ const strictMockApi = asRecord(strictRecord.mockApi);
899
+ const strictTargetFiles = asStringArray(asRecord(strictRecord.targets)?.files);
900
+ const mockEndpoints = Array.isArray(strictMockApi?.endpoints)
901
+ ? strictMockApi.endpoints.map((item) => {
902
+ const endpoint = asRecord(item);
903
+ if (!endpoint)
904
+ return item;
905
+ return {
906
+ ...endpoint,
907
+ consumer: asString(endpoint.consumer) ||
908
+ strictTargetFiles[0],
909
+ };
910
+ })
911
+ : strictMockApi?.endpoints;
912
+ return {
913
+ ...strictRecord,
914
+ requirements,
915
+ ...(strictMockApi ? { mockApi: { ...strictMockApi, endpoints: mockEndpoints } } : {}),
916
+ };
734
917
  }
735
918
  }
736
- const record = asRecord(normalizedValue);
919
+ const record = asRecord(normalizedOptionalFields);
737
920
  if (!record)
738
921
  return value;
739
922
  const implementation = asRecord(record.implementation);
@@ -1069,10 +1252,10 @@ export async function materializeFrontendImplementationContract(input) {
1069
1252
  const normalizedContract = coerceFrontendImplementationContractInput(parsed, canonicalBinding);
1070
1253
  // There is exactly one post-security candidate. A fallback candidate would
1071
1254
  // allow malformed raw fields to bypass the boundary checks above.
1072
- const candidate = {
1255
+ const candidate = deriveFrontendVerificationCoverage({
1073
1256
  ...(asRecord(normalizedContract) ?? parsed),
1074
1257
  sourceBinding: canonicalBinding,
1075
- };
1258
+ }, canonicalBinding);
1076
1259
  const result = frontendImplementationContractSchema.safeParse(candidate);
1077
1260
  if (!result.success)
1078
1261
  throw new Error(`invalid-output: ${result.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")}`);
@@ -1300,9 +1300,19 @@ async function discoverFrontendFallbackVerifyCommands(repoRoot) {
1300
1300
  "test:e2e",
1301
1301
  "e2e",
1302
1302
  ]);
1303
+ if (staticCommands.length === 0 && behaviorCommands.length === 0) {
1304
+ try {
1305
+ await access(path.join(repoRoot, "node_modules", ".bin", "tsc"));
1306
+ staticCommands.push("npx --no-install tsc --noEmit");
1307
+ }
1308
+ catch {
1309
+ // No local TypeScript binary: leave verification unavailable rather
1310
+ // than invoking a missing script or downloading a package.
1311
+ }
1312
+ }
1303
1313
  return {
1304
- staticCommands: staticCommands.length > 0 ? staticCommands : behaviorCommands,
1305
- behaviorCommands: behaviorCommands.length > 0 ? behaviorCommands : staticCommands,
1314
+ staticCommands: staticCommands.length > 0 ? staticCommands : [],
1315
+ behaviorCommands: behaviorCommands.length > 0 ? behaviorCommands : [],
1306
1316
  };
1307
1317
  }
1308
1318
  function deriveFrontendBehaviorPaths(taskConfig) {
@@ -5956,12 +5966,12 @@ async function buildSoftVerifyNode(sources) {
5956
5966
  const strategy = resolveDagVerifyStrategy(sources.taskConfig, "1");
5957
5967
  const fallbackCommands = sources.repoRoot
5958
5968
  ? (await discoverFrontendFallbackVerifyCommands(sources.repoRoot)).staticCommands
5959
- : ["npm run typecheck"];
5969
+ : [];
5960
5970
  const candidatePlan = canonicalizeVerificationCommands([
5961
5971
  ...taskVerifyCommands(sources.repoRoot, sources.taskConfig),
5962
5972
  ...(sources.verifyCommands?.intermediate ?? []),
5963
5973
  ]);
5964
- const focusedCandidates = candidatePlan.commands.filter((command) => !isFullSuiteVerifyCommand(command));
5974
+ const focusedCandidates = candidatePlan.commands.filter((command) => !isFullSuiteVerifyCommand(command) && !isFrontendLintVerifyCommand(command));
5965
5975
  const quota = verifyQuotaLimit(strategy.intermediateQuota ?? "full");
5966
5976
  const focusedIntermediate = quota
5967
5977
  ? focusedCandidates.slice(0, quota)
@@ -5971,11 +5981,14 @@ async function buildSoftVerifyNode(sources) {
5971
5981
  commands: focusedIntermediate,
5972
5982
  taskConfig: sources.taskConfig,
5973
5983
  });
5974
- const commands = buildVerifyShellCommands({
5984
+ const commandsWithoutFallback = buildVerifyShellCommands({
5975
5985
  repoRoot: sources.repoRoot,
5976
5986
  commands: plannedIntermediate.commands,
5977
5987
  fallbackCommands,
5978
5988
  });
5989
+ const commands = commandsWithoutFallback.length > 0
5990
+ ? commandsWithoutFallback
5991
+ : ["node -e \"console.log('No project verification command configured; verification skipped')\""];
5979
5992
  return {
5980
5993
  id: "soft-verify-shell",
5981
5994
  depends_on: [implementId],
@@ -5994,7 +6007,7 @@ async function buildSoftVerifyNode(sources) {
5994
6007
  quota: strategy.intermediateQuota ?? "full",
5995
6008
  commandSource: sources.verifyCommands ? "adapter" : "inline",
5996
6009
  commands: plannedIntermediate.commands,
5997
- fallbackCommands,
6010
+ fallbackCommands: commands,
5998
6011
  commandTexts: commands,
5999
6012
  commandTimeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
6000
6013
  plan: candidatePlan,
@@ -6094,9 +6107,13 @@ function buildRepairNode(sources) {
6094
6107
  .join("\n\n"),
6095
6108
  };
6096
6109
  }
6097
- function buildHardVerifyNode(sources) {
6110
+ async function buildHardVerifyNode(sources) {
6098
6111
  const repairId = repairNodeId();
6112
+ const discoveredFallback = sources.repoRoot
6113
+ ? await discoverFrontendFallbackVerifyCommands(sources.repoRoot)
6114
+ : { staticCommands: [], behaviorCommands: [] };
6099
6115
  const fallbackCommands = [
6116
+ ...discoveredFallback.staticCommands.filter((command) => /(?:^|\s)(?:npm|pnpm|yarn|bun)\s+run\s+build(?:\s|$)/.test(command)),
6100
6117
  "HARNESS_ALLOW_ACTIVE_DAG_RUNS=1 bash scripts/check-repo.sh",
6101
6118
  ];
6102
6119
  const finalPlan = canonicalizeVerificationCommands([
@@ -6105,7 +6122,7 @@ function buildHardVerifyNode(sources) {
6105
6122
  ]);
6106
6123
  const plannedFinal = applyMavenVerificationPlanning({
6107
6124
  repoRoot: sources.repoRoot,
6108
- commands: finalPlan.commands,
6125
+ commands: finalPlan.commands.filter((command) => !isFrontendLintVerifyCommand(command)),
6109
6126
  taskConfig: sources.taskConfig,
6110
6127
  });
6111
6128
  const commands = buildVerifyShellCommands({
@@ -6274,7 +6291,7 @@ async function buildSupervisedHybridDag(standard, sources) {
6274
6291
  buildProcessSupervisorNode(sources),
6275
6292
  buildProcessGateNode(sources),
6276
6293
  buildRepairNode(sources),
6277
- buildHardVerifyNode(sources),
6294
+ await buildHardVerifyNode(sources),
6278
6295
  ...(authorityDecision.enabled
6279
6296
  ? [
6280
6297
  buildAuthoritySurfaceAuditNode(sources),
@@ -62,23 +62,76 @@ export function firstNonEmptyLine(text) {
62
62
  }
63
63
  return undefined;
64
64
  }
65
- /**
66
- * Parse the JSON-only review verdict protocol. The complete trimmed output
67
- * must be exactly one JSON object: fences, prose, partial objects, and
68
- * concatenated objects are all rejected before schema validation.
69
- */
65
+ function extractUniqueReviewJson(text) {
66
+ const source = String(text).trim();
67
+ const blocks = [...source.matchAll(/```(?:json|jsonc)?\s*([\s\S]*?)\s*```/gi)].map((match) => match[1].trim());
68
+ const candidates = blocks.length > 0 ? blocks : [source];
69
+ const parsed = [];
70
+ for (const candidate of candidates) {
71
+ let cursor = 0;
72
+ while (cursor < candidate.length) {
73
+ const start = candidate.indexOf("{", cursor);
74
+ if (start < 0)
75
+ break;
76
+ let depth = 0;
77
+ let inString = false;
78
+ let escaped = false;
79
+ let end = -1;
80
+ for (let index = start; index < candidate.length; index += 1) {
81
+ const character = candidate[index];
82
+ if (inString) {
83
+ if (escaped)
84
+ escaped = false;
85
+ else if (character === "\\")
86
+ escaped = true;
87
+ else if (character === '"')
88
+ inString = false;
89
+ continue;
90
+ }
91
+ if (character === '"')
92
+ inString = true;
93
+ else if (character === "{")
94
+ depth += 1;
95
+ else if (character === "}" && --depth === 0) {
96
+ try {
97
+ const value = JSON.parse(candidate.slice(start, index + 1));
98
+ if (value && typeof value === "object" && !Array.isArray(value))
99
+ parsed.push(value);
100
+ }
101
+ catch {
102
+ // Keep fail-closed behavior for malformed JSON.
103
+ }
104
+ end = index;
105
+ break;
106
+ }
107
+ }
108
+ if (end < 0)
109
+ break;
110
+ cursor = end + 1;
111
+ }
112
+ }
113
+ if (parsed.length !== 1) {
114
+ throw new Error(parsed.length === 0
115
+ ? "output must contain one valid JSON review object"
116
+ : "output must contain exactly one JSON review object");
117
+ }
118
+ return parsed[0];
119
+ }
120
+ /** Parse a JSON review verdict after extracting one unambiguous JSON object.
121
+ * Prose and one Markdown fence are tolerated at this boundary; ambiguity and
122
+ * schema/semantic violations remain fail-closed. */
70
123
  export function parseJsonReviewVerdict(text) {
71
124
  const trimmed = String(text).trim();
72
125
  if (!trimmed)
73
126
  return { ok: false, reason: "missing JSON output" };
74
127
  let parsed;
75
128
  try {
76
- parsed = JSON.parse(trimmed);
129
+ parsed = extractUniqueReviewJson(trimmed);
77
130
  }
78
131
  catch (error) {
79
132
  return {
80
133
  ok: false,
81
- reason: `output must be exactly one JSON object: ${error instanceof Error ? error.message : String(error)}`,
134
+ reason: `output JSON extraction failed: ${error instanceof Error ? error.message : String(error)}`,
82
135
  };
83
136
  }
84
137
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
@@ -99,7 +99,7 @@ loop-agent task advance <task-id> "任务标题" \
99
99
  --prd <path-to-prd.md> \
100
100
  --allowed-path "<glob>" \
101
101
  --forbidden-path ".harness/**" \
102
- --verify "typecheck:npm run typecheck" \
102
+ --verify "<label>:<command>" \
103
103
  --json
104
104
  # 审查 writeSet gate digest 后:
105
105
  loop-agent task advance <task-id> --approve-gate "write-set-review:<digest>" --json
@@ -108,6 +108,8 @@ loop-agent task status <task-id> --json
108
108
  # 有 plan 时收尾:loop-agent plan complete <plan-id> --summary "..."
109
109
  ```
110
110
 
111
+ `--verify` 命令应取项目 `AGENTS.md` / `__LOOP_AGENT_GOVERNANCE_ROOT__/verification-matrix.md` 登记的验证命令(不要假定 `npm run typecheck` 存在);`--verify` 可选,省略时自动从 package.json scripts 或既有 managed `task.json.verifyCommands` 推导建议。
112
+
111
113
  `source/需求.md` 与 `source/执行约束.md` 仍必需(M8/M9),但默认由 `task advance --prd` 投影生成,而不是主会话手写。`plan create` 不是 `task advance` 的硬依赖。写入前同步 `task.json.allowedPaths` / `task.json.forbiddenPaths` 并审查 writer `writeSet`。高级任意 DagSpec 才用 `dag validate|execute|report`,不进入标准 happy path。
112
114
 
113
115
  凡是影响项目公共契约、执行入口、交付流水线、自动化/治理、数据模型、安全或权限模型、跨模块行为、用户可见工作流的改动,都必须在编辑实现文件前先通过 `task advance` 建立 managed contract / writeSet gate,并审查 gate digest。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tea-agent/loop-agent",
3
- "version": "0.28.11",
3
+ "version": "0.28.12",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "loop-agent": "bin/loop-agent.js",
@@ -32,12 +32,14 @@ references:
32
32
  ```bash
33
33
  loop-agent task advance <task-id> "Title" --prd <prd.md> \
34
34
  --allowed-path "<glob>" --forbidden-path ".harness/**" \
35
- --verify "typecheck:npm run typecheck" --json
35
+ --verify "<label>:<command>" --json
36
36
  loop-agent task advance <task-id> \
37
37
  --approve-gate "write-set-review:<digest>" --json
38
38
  loop-agent task status <task-id> --json
39
39
  ```
40
40
 
41
+ `--verify` 命令应取项目 `AGENTS.md` / 治理文档 verification-matrix.md 登记的验证命令(不要假定 `npm run typecheck` 存在);`--verify` 可选,省略时自动从 package.json scripts 或既有 managed `task.json.verifyCommands` 推导建议。
42
+
41
43
  非微小跨会话任务可先 `plan create`。主会话只审 gate/writeSet 与验证证据,不拼低层 prepare/run-task/run-dag/promote/closeout 命令串。
42
44
 
43
45
  ## 主题路由
@@ -42,7 +42,7 @@ loop-agent doctor
42
42
  --accept-recommendations DIGEST \
43
43
  --allowed-path "src/**" \
44
44
  --forbidden-path ".harness/**" \
45
- --verify "typecheck:npm run typecheck" \
45
+ --verify "<label>:<command>" \
46
46
  --json
47
47
 
48
48
  # 审查 gate.writeSet / gate.digest 后
@@ -55,6 +55,8 @@ loop-agent doctor
55
55
  loop-agent plan complete PLAN_ID --summary "..."
56
56
  ```
57
57
 
58
+ `--verify` 命令应取项目 `AGENTS.md` / `docs/governance/verification-matrix.md` 登记的验证命令(不要假定 `npm run typecheck` 存在);`--verify` 可选,省略时自动从 package.json scripts 或既有 managed `task.json.verifyCommands` 推导建议。
59
+
58
60
  首次 `task advance` 内部完成 create、PRD 归档、managed contract 投影、DAG 生成与 strict validate,并停在 writeSet gate;批准后同一命令长跑到稳定终态并做确定性 promotion/closeout。决策表与反模式见 `source-and-plan-practice.md`。
59
61
  2. **Operator 工具**,用于 recovery、诊断、评测重放:
60
62
 
@@ -166,7 +168,7 @@ loop-agent examples copy <name> --output examples/<name>
166
168
  loop-agent task advance <task-id> "Task Title" \
167
169
  --prd path/to-prd.md \
168
170
  --allowed-path "src/**" \
169
- --verify "typecheck:npm run typecheck" \
171
+ --verify "<label>:<command>" \
170
172
  --json
171
173
  loop-agent task advance <task-id> --approve-gate "write-set-review:<digest>" --json
172
174
  loop-agent task status <task-id> --json
@@ -218,7 +220,7 @@ loop-agent task advance <task-id> "标题" \
218
220
  --prd <path-to-original-prd.md> \
219
221
  --allowed-path "<glob>" \
220
222
  --forbidden-path ".harness/**" \
221
- --verify "typecheck:npm run typecheck" \
223
+ --verify "<label>:<command>" \
222
224
  --json
223
225
  ```
224
226
 
@@ -566,6 +568,9 @@ agent-worker scheduler cancel <schedule-id> --repo <repo-root> [--reason <text>]
566
568
  agent-worker scheduler harvest <schedule-id> --repo <repo-root> [--json]
567
569
  agent-worker scheduler discard <schedule-id> --repo <repo-root> --reason <text> [--force] [--json]
568
570
  agent-worker scheduler tick --repo <repo-root> [--json] # daemon/clock; not Operator Chat
571
+ agent-worker scheduler clock install --repo <repo-root> [--interval-sec 60] [--loop-agent-bin <path>] [--json] # OS timer; CLI-only
572
+ agent-worker scheduler clock status --repo <repo-root> [--json] # CLI-only
573
+ agent-worker scheduler clock uninstall --repo <repo-root> [--json] # CLI-only
569
574
  agent-worker scheduler submit --repo <repo-root> --feature-id <id> --task-id <id> ... # low-level planning fact
570
575
  agent-worker scheduler transition <schedule-id> --repo <repo-root> --to <status> ... # internal lifecycle
571
576
  agent-worker console [--repo <repo-root>] [--port 8790] [--host 127.0.0.1] # Official 裸入口;repo 默认当前目录
@@ -596,7 +601,7 @@ agent-worker observe snapshot --repo <repo-root> # 输出 GlobalSnapshot JSON
596
601
  - `task draft-followup` 会按全部 failure category 生成 TaskDraft 或人工行动卡:ProductBug/TestBug/FlakyTest/DependencyFailure 可批准;EnvFailure 连续两次后才生成 ENV-CHECK;Spec/Contract/Risk/Human/Unknown 只给行动卡。人工以 `feature approve-followup --dry-run` 预览,再带非空 `--owner` 批准 TaskDraft;行动卡不能批准。批准在 staging validation 后写 TaskSpec、graph、Ready/approval/event,原失败事实不改写,并有 rename/state/approval/index/event 回滚门禁。
597
602
  - `task retry` 是失败 Task 的唯一重试入口。它会保留原有运行记录和 failure handoff,并让下一次 `batch run-ready` 使用新的 `workerRunId`;不要删除运行态文件或手动修改状态来重试。
598
603
  - **推荐**裸入口 `agent-worker console`(repo 默认当前目录,默认 `127.0.0.1:8790`)提供 Operate + Inspect;`console serve` 是等价兼容入口。Inspect 路径为 `/inspect/#/...`,API 仍为根 `/api/**`。`observe serve` 为兼容入口(默认 `8787`,启动时 stderr 输出 `OBSERVE_SERVE_DEPRECATED`);`observe snapshot` 保留。Inspect/Observe 本身不会启动、暂停或重试 Task / Worker / DAG。
599
- - Night Scheduler(本地夜间自治):白天 `admission prepare` 冻结 worktree + DAG writeSet gate;`scheduler add` 消费 gate 并预约 Task Pool `Queued`(不立即执行);`scheduler tick` 作为 Worker 时钟 claim/dispatchCLI/cron,不进 Operator Chat);成功后 `pending-harvest`,早晨 `scheduler harvest`(exact-base FF)或 `scheduler discard`;`report morning --window night` 与 Inspect `#/night` / Operate 夜间面板读取同一套 facts。
604
+ - Night Scheduler(本地夜间自治):白天 `admission prepare` 冻结 worktree + DAG writeSet gate;`scheduler add` 消费 gate 并预约 Task Pool `Queued`(不立即执行);`scheduler clock install` 安装本机 user-level OS timerlaunchd / systemd --user / schtasks)周期调用 `scheduler tick`;tick 写统一 Clock receipt,doctor/morning/status 可解释 missing/stale/error/busy/drift;成功后 `pending-harvest`,早晨 `scheduler harvest`(exact-base FF)或 `scheduler discard`;`report morning --window night` 与 Inspect `#/night` / Operate 夜间面板读取同一套 facts。Clock install/status/uninstall 为 CLI-only。
600
605
  - 当前 Worker 仍是 v0(库 + CLI + dogfood);日间批处理未强制定时/CI 驱动;`report morning` 默认可从 Task Pool runs 汇总,并支持 `--window night` 投影 Scheduler facts。
601
606
 
602
607
  ### 查看 duration statistics / context usage
@@ -57,7 +57,7 @@ Minimum governed path:
57
57
  loop-agent task advance <task-id> "Task Title" \
58
58
  --prd <path-to-original-prd.md> \
59
59
  --allowed-path "<glob>" \
60
- --verify "typecheck:npm run typecheck" \
60
+ --verify "<label>:<command>" \
61
61
  --json \
62
62
  [--repo-root <target-repo>]
63
63
 
@@ -70,6 +70,8 @@ loop-agent task advance <task-id> \
70
70
  loop-agent task status <task-id> --json [--repo-root <target-repo>]
71
71
  ```
72
72
 
73
+ The `--verify` command should reference verification commands registered in the project's `AGENTS.md` or its governance verification-matrix.md; do not assume `npm run typecheck` exists. `--verify` is optional; when omitted, suggestions are auto-derived from `package.json` scripts or existing managed `task.json.verifyCommands`.
74
+
73
75
  `loop-agent` is the preferred global CLI. For self-hosting loop-agent development, the controller must be an installed npm-published package. Use `npm install -g @tea-agent/loop-agent@latest` for first install or intentional upgrades, then treat the installed version as frozen for the current task and record `npm list -g @tea-agent/loop-agent --depth=0`. Do not repeatedly fetch `npx @latest` inside DAG nodes, and do not use the current working tree's `npm link` or `npm run dev` to control tasks that may edit CLI, DAG runtime, executors, package metadata, or build output. Use `npm run dev -- <args>` only for source debugging and focused CLI development.
74
76
 
75
77
  The npm package carries static capability assets: `skills/` (bundled in-package, containing `loop-agent` and `agent-worker`; `loop-agent init` mirrors them into the target project's `.agents/skills/`), `docs/templates/`, `docs/architecture/`, `docs/skills/`, `examples/`, `harness.json`, `AGENTS.md`, `README.md`, and `CHANGELOG.md`. `loop-agent init` creates `ai_workspace/loop-agent/` (the default governanceRoot) in the target project with `progress/`, `reports/`, `exec-plans/`, and `decisions/` directories plus their README files; the actual files under those directories belong to the target repository and are not shipped by the npm package.