@tea-agent/loop-agent 0.11.0 → 0.13.0-alpha.0

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 (123) hide show
  1. package/CHANGELOG.md +74 -1
  2. package/README.md +33 -4
  3. package/dist/application/dag/generate-task-dag.js +45 -0
  4. package/dist/application/dag/run-dag.js +10 -0
  5. package/dist/application/dag/validate-dag.js +11 -0
  6. package/dist/cli/command-definitions.js +10 -3
  7. package/dist/commands/init.js +74 -7
  8. package/dist/commands/knowledge.js +129 -31
  9. package/dist/governance/manifest-types.js +3 -0
  10. package/dist/shared/package-metadata.js +135 -0
  11. package/dist/task/config-types.js +6 -1
  12. package/dist/worker/cli.js +99 -2
  13. package/dist/worker/delivery/package.js +3 -3
  14. package/dist/worker/feature/decision-loader.js +37 -6
  15. package/dist/worker/feature/next-action.js +10 -2
  16. package/dist/worker/feature/ready-plan-projection.js +81 -0
  17. package/dist/worker/feature/reducer.js +2 -1
  18. package/dist/worker/feature/review.js +19 -2
  19. package/dist/worker/feature/run.js +27 -2
  20. package/dist/worker/follow-up/approve.js +5 -2
  21. package/dist/worker/follow-up/factory.js +1 -1
  22. package/dist/worker/observability/event-history.js +216 -0
  23. package/dist/worker/observability/read-model.js +552 -118
  24. package/dist/worker/observe/paths.js +17 -0
  25. package/dist/worker/observe/routes.js +310 -23
  26. package/dist/worker/observe/server.js +59 -1
  27. package/dist/worker/observe/spec-evidence.js +281 -0
  28. package/dist/worker/observe/static/api.js +46 -0
  29. package/dist/worker/observe/static/app.js +120 -2598
  30. package/dist/worker/observe/static/constants.js +148 -0
  31. package/dist/worker/observe/static/copy.js +67 -0
  32. package/dist/worker/observe/static/dag-helpers.js +172 -0
  33. package/dist/worker/observe/static/dag-model.js +72 -0
  34. package/dist/worker/observe/static/dom.js +61 -0
  35. package/dist/worker/observe/static/format-pool.js +67 -0
  36. package/dist/worker/observe/static/format.js +292 -0
  37. package/dist/worker/observe/static/index.html +300 -82
  38. package/dist/worker/observe/static/kpi.js +94 -0
  39. package/dist/worker/observe/static/relations.js +133 -0
  40. package/dist/worker/observe/static/router.js +93 -0
  41. package/dist/worker/observe/static/run-processing.js +148 -0
  42. package/dist/worker/observe/static/shell-chrome.js +68 -0
  43. package/dist/worker/observe/static/state.js +253 -0
  44. package/dist/worker/observe/static/styles.css +1731 -495
  45. package/dist/worker/observe/static/views/batch.js +227 -0
  46. package/dist/worker/observe/static/views/dag-graph.js +172 -0
  47. package/dist/worker/observe/static/views/dag-inspector.js +596 -0
  48. package/dist/worker/observe/static/views/dag.js +362 -0
  49. package/dist/worker/observe/static/views/dashboard.js +445 -0
  50. package/dist/worker/observe/static/views/failures.js +143 -0
  51. package/dist/worker/observe/static/views/feature.js +492 -0
  52. package/dist/worker/observe/static/views/pool.js +350 -0
  53. package/dist/worker/observe/static/views/run.js +453 -0
  54. package/dist/worker/observe/static/views/session-timeline.js +205 -0
  55. package/dist/worker/observe/static/views/shell.js +7 -0
  56. package/dist/worker/observe/static/views/task.js +314 -0
  57. package/dist/worker/observe/static/views/timeline.js +163 -0
  58. package/dist/worker/pool/doctor.js +165 -0
  59. package/dist/worker/pool/migrate-state.js +303 -0
  60. package/dist/worker/pool/run-store.js +205 -17
  61. package/dist/worker/pool/types.js +17 -1
  62. package/dist/worker/pool/validation.js +100 -15
  63. package/dist/worker/report/morning-report.js +12 -2
  64. package/dist/worker/runner/run-ready.js +41 -26
  65. package/dist/worker/task-graph/ready-planner.js +136 -0
  66. package/dist/workflows/dag/controller-identity.js +104 -0
  67. package/dist/workflows/dag/convergence/controller.js +16 -8
  68. package/dist/workflows/dag/failure-routing.js +12 -1
  69. package/dist/workflows/dag/init-hybrid.js +1233 -11
  70. package/dist/workflows/dag/node-execution.js +123 -29
  71. package/dist/workflows/dag/repair-artifact.js +91 -0
  72. package/dist/workflows/dag/report.js +50 -0
  73. package/dist/workflows/dag/retry-policy.js +138 -0
  74. package/dist/workflows/dag/runner.js +32 -0
  75. package/dist/workflows/dag/runtime-contract.js +87 -0
  76. package/dist/workflows/dag/skill-snapshot.js +2 -0
  77. package/dist/workflows/dag/types.js +45 -1
  78. package/dist/workflows/dag/validate.js +68 -4
  79. package/docs/README.md +1 -1
  80. package/docs/agent-dag-recovery-playbook.md +9 -0
  81. package/docs/agent-dag-runner.md +26 -1
  82. package/docs/architecture/dag-execution.md +6 -0
  83. package/docs/architecture/evolution.md +7 -5
  84. package/docs/architecture/facts-and-state.md +15 -2
  85. package/docs/architecture/worker-and-feature.md +6 -2
  86. package/docs/decisions/README.md +3 -0
  87. package/docs/design/README.md +12 -3
  88. package/docs/exec-plans/active/README.md +2 -2
  89. package/docs/exec-plans/completed/README.md +12 -0
  90. package/docs/feature-workflow.md +108 -2
  91. package/docs/loop-agent-harness.md +15 -4
  92. package/docs/progress/README.md +22 -0
  93. package/docs/reports/README.md +14 -2
  94. package/docs/templates/agent-dag-report.schema.json +17 -0
  95. package/docs/templates/agent-dag.schema.json +69 -1
  96. package/docs/templates/agent-dag.supervised-implementation.json +8 -2
  97. package/docs/templates/backend-test-dag.generate-pytest.prompt.md +139 -0
  98. package/docs/templates/backend-test-dag.json +288 -0
  99. package/docs/templates/backend-test-dag.retrospect.prompt.md +125 -0
  100. package/docs/templates/backend-test-dag.review-cases.prompt.md +81 -0
  101. package/docs/templates/knowledge-graph-bootstrap-dag.json +118 -0
  102. package/docs/templates/knowledge-sync-dag.json +177 -0
  103. package/docs/templates/knowledge-sync-draft.schema.json +71 -0
  104. package/docs/verification-matrix.md +2 -1
  105. package/package.json +8 -2
  106. package/scripts/kb-bootstrap-init-skeleton.sh +239 -0
  107. package/scripts/kb-graph-incremental-prepare.mjs +372 -0
  108. package/scripts/kb-graph-incremental-prepare.sh +5 -0
  109. package/scripts/kb-graph-materialize.mjs +105 -0
  110. package/scripts/kb-graph-materialize.sh +4 -0
  111. package/scripts/kb-graph-promote.mjs +153 -0
  112. package/scripts/kb-graph-promote.sh +4 -0
  113. package/scripts/kb-query.mjs +554 -0
  114. package/scripts/kb-query.sh +5 -0
  115. package/skills/agent-worker/SKILL.md +3 -1
  116. package/skills/agent-worker/references/agent-worker-operator.md +18 -1
  117. package/skills/frontend-design-review/SKILL.md +26 -24
  118. package/skills/frontend-implementation/SKILL.md +29 -26
  119. package/skills/frontend-implementation/references/node-contracts.md +50 -19
  120. package/skills/frontend-review/SKILL.md +1 -1
  121. package/skills/loop-agent/references/command-reference.md +2 -0
  122. package/skills/loop-agent/references/hybrid-dag.md +22 -3
  123. package/skills/loop-agent/references/verification-and-failure-handling.md +6 -0
@@ -1,41 +1,104 @@
1
1
  import path from "node:path";
2
+ import { spawnSync } from "node:child_process";
3
+ import { fileURLToPath } from "node:url";
4
+ import { existsSync } from "node:fs";
2
5
  import { curateKnowledgePatterns } from "../workflows/dag/knowledge-curator.js";
6
+ export const KNOWLEDGE_CLI_SUBCOMMANDS = [
7
+ "curate",
8
+ "query",
9
+ "graph-init",
10
+ "graph-materialize",
11
+ "graph-promote",
12
+ "graph-incremental-prepare",
13
+ ];
14
+ const USAGE = "usage: knowledge <curate|query|graph-init|graph-materialize|graph-promote|graph-incremental-prepare> ...";
15
+ function packageRoot() {
16
+ // dist/commands/knowledge.js or src/commands/knowledge.ts → package/repo root
17
+ return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
18
+ }
19
+ function resolveScript(repoRoot, fileName) {
20
+ const candidates = [
21
+ path.join(repoRoot, "scripts", fileName),
22
+ path.join(packageRoot(), "scripts", fileName),
23
+ ];
24
+ for (const c of candidates) {
25
+ if (existsSync(c))
26
+ return c;
27
+ }
28
+ throw new Error(`knowledge script not found: ${fileName} (looked under repo scripts/ and package scripts/)`);
29
+ }
30
+ function runProcess(command, args, cwd) {
31
+ const result = spawnSync(command, args, {
32
+ cwd,
33
+ encoding: "utf8",
34
+ stdio: ["inherit", "pipe", "pipe"],
35
+ });
36
+ if (result.stdout)
37
+ process.stdout.write(result.stdout);
38
+ if (result.stderr)
39
+ process.stderr.write(result.stderr);
40
+ if (result.error)
41
+ throw result.error;
42
+ if (result.status !== 0 && result.status !== null) {
43
+ process.exitCode = result.status;
44
+ }
45
+ }
46
+ function runNodeScript(repoRoot, scriptFile, forwardArgs) {
47
+ const scriptPath = resolveScript(repoRoot, scriptFile);
48
+ runProcess(process.execPath, [scriptPath, ...forwardArgs], repoRoot);
49
+ }
50
+ function runBashScript(repoRoot, scriptFile, forwardArgs) {
51
+ const scriptPath = resolveScript(repoRoot, scriptFile);
52
+ runProcess("bash", [scriptPath, ...forwardArgs], repoRoot);
53
+ }
54
+ function ensureRootFlag(repoRoot, rest) {
55
+ if (rest.includes("--root"))
56
+ return rest;
57
+ return ["--root", repoRoot, ...rest];
58
+ }
3
59
  export function parseKnowledgeArgs(args) {
4
60
  const [command, ...rest] = args;
5
- if (command !== "curate") {
6
- throw new Error("usage: knowledge curate [--json|--markdown] [--output <path>]");
61
+ if (!command ||
62
+ !KNOWLEDGE_CLI_SUBCOMMANDS.includes(command)) {
63
+ throw new Error(USAGE);
7
64
  }
8
- let outputPath;
9
- let json = false;
10
- let markdown = false;
11
- for (let i = 0; i < rest.length; i += 1) {
12
- const arg = rest[i];
13
- if (arg === "--json") {
14
- json = true;
15
- continue;
16
- }
17
- if (arg === "--markdown") {
18
- markdown = true;
19
- continue;
65
+ if (command === "curate") {
66
+ let outputPath;
67
+ let json = false;
68
+ let markdown = false;
69
+ for (let i = 0; i < rest.length; i += 1) {
70
+ const arg = rest[i];
71
+ if (arg === "--json") {
72
+ json = true;
73
+ continue;
74
+ }
75
+ if (arg === "--markdown") {
76
+ markdown = true;
77
+ continue;
78
+ }
79
+ if (arg === "--output") {
80
+ outputPath = rest[++i];
81
+ if (!outputPath) {
82
+ throw new Error("knowledge curate --output requires a path");
83
+ }
84
+ continue;
85
+ }
86
+ if (arg.startsWith("--output=")) {
87
+ outputPath = arg.slice("--output=".length);
88
+ continue;
89
+ }
90
+ throw new Error(`unknown knowledge curate argument: ${arg}`);
20
91
  }
21
- if (arg === "--output") {
22
- outputPath = rest[++i];
23
- if (!outputPath)
24
- throw new Error("knowledge curate --output requires a path");
25
- continue;
26
- }
27
- if (arg.startsWith("--output=")) {
28
- outputPath = arg.slice("--output=".length);
29
- continue;
30
- }
31
- throw new Error(`unknown knowledge curate argument: ${arg}`);
92
+ if (!json && !markdown)
93
+ json = true;
94
+ return { command: "curate", outputPath, json, markdown };
32
95
  }
33
- if (!json && !markdown)
34
- json = true;
35
- return { command, outputPath, json, markdown };
96
+ return {
97
+ command: command,
98
+ forward: rest,
99
+ };
36
100
  }
37
- export async function runKnowledge(repoRoot, args) {
38
- const parsed = parseKnowledgeArgs(args);
101
+ async function runCurate(repoRoot, parsed) {
39
102
  const result = await curateKnowledgePatterns({
40
103
  repoRoot,
41
104
  outputPath: parsed.outputPath,
@@ -50,7 +113,9 @@ export async function runKnowledge(repoRoot, args) {
50
113
  console.log(JSON.stringify({
51
114
  ok: result.ok,
52
115
  patternsPath: path.relative(repoRoot, result.patternsPath),
53
- outputPath: result.outputPath ? path.relative(repoRoot, result.outputPath) : undefined,
116
+ outputPath: result.outputPath
117
+ ? path.relative(repoRoot, result.outputPath)
118
+ : undefined,
54
119
  patternCount: result.patternCount,
55
120
  safetyFindings: result.safetyFindings,
56
121
  message: result.message,
@@ -62,3 +127,36 @@ export async function runKnowledge(repoRoot, args) {
62
127
  process.stdout.write(result.proposalMarkdown);
63
128
  }
64
129
  }
130
+ /**
131
+ * knowledge CLI:
132
+ * - curate: repair/learned guidance proposals (existing)
133
+ * - query / graph-*: thin wrappers over scripts/kb-*.{mjs,sh} for graph KB ops
134
+ */
135
+ export async function runKnowledge(repoRoot, args) {
136
+ const parsed = parseKnowledgeArgs(args);
137
+ if (parsed.command === "curate") {
138
+ await runCurate(repoRoot, parsed);
139
+ return;
140
+ }
141
+ const forward = ensureRootFlag(repoRoot, parsed.forward);
142
+ switch (parsed.command) {
143
+ case "query":
144
+ runNodeScript(repoRoot, "kb-query.mjs", forward);
145
+ return;
146
+ case "graph-init":
147
+ // B1 skeleton is a bash script (no LLM)
148
+ runBashScript(repoRoot, "kb-bootstrap-init-skeleton.sh", forward);
149
+ return;
150
+ case "graph-materialize":
151
+ runNodeScript(repoRoot, "kb-graph-materialize.mjs", forward);
152
+ return;
153
+ case "graph-promote":
154
+ runNodeScript(repoRoot, "kb-graph-promote.mjs", forward);
155
+ return;
156
+ case "graph-incremental-prepare":
157
+ runNodeScript(repoRoot, "kb-graph-incremental-prepare.mjs", forward);
158
+ return;
159
+ default:
160
+ throw new Error(USAGE);
161
+ }
162
+ }
@@ -43,6 +43,9 @@ export const workflowPolicyDagTemplateSchema = z.enum([
43
43
  "review-gated-dag",
44
44
  "supervised-implementation",
45
45
  "frontend-implementation",
46
+ "backend-test-dag",
47
+ "knowledge-sync-dag",
48
+ "knowledge-graph-bootstrap-dag",
46
49
  ]);
47
50
  export const workflowPolicyOutputLanguageSchema = z.enum(["zh-CN", "en"]);
48
51
  export const workflowPolicySchema = z
@@ -2,7 +2,9 @@ import { createHash } from "node:crypto";
2
2
  import { readFileSync, readdirSync, statSync } from "node:fs";
3
3
  import { realpathSync } from "node:fs";
4
4
  import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
5
6
  import { platform } from "node:os";
7
+ export const LOOP_AGENT_PACKAGE_NAME = "@tea-agent/loop-agent";
6
8
  // ---------------------------------------------------------------------------
7
9
  // Public helpers
8
10
  // ---------------------------------------------------------------------------
@@ -351,3 +353,136 @@ function assertRegularFile(filePath, requested) {
351
353
  }
352
354
  throw new Error(`controller identity: resolved executable for ${JSON.stringify(requested)} is not a readable file: ${filePath}`);
353
355
  }
356
+ // ---------------------------------------------------------------------------
357
+ // Running-controller identity (in-process capture)
358
+ // ---------------------------------------------------------------------------
359
+ function safeRealpath(filePath) {
360
+ try {
361
+ return realpathSync(filePath);
362
+ }
363
+ catch {
364
+ return filePath;
365
+ }
366
+ }
367
+ /**
368
+ * Build a ControllerIdentityV1 from a readable entry file inside a Node package
369
+ * root. Returns `undefined` when any required identity component cannot be
370
+ * resolved. This reuses the same ControllerIdentityV1 protocol the Worker uses;
371
+ * it never invents a second identity shape.
372
+ */
373
+ export function buildControllerIdentityFromEntry(input) {
374
+ const resolvedAt = (input.now ?? new Date()).toISOString();
375
+ const realEntry = safeRealpath(path.resolve(input.entry));
376
+ try {
377
+ if (!statSync(realEntry).isFile())
378
+ return undefined;
379
+ }
380
+ catch {
381
+ return undefined;
382
+ }
383
+ const packageRoot = findPackageRoot(path.dirname(realEntry));
384
+ if (!packageRoot)
385
+ return undefined;
386
+ const packageName = readPackageName(packageRoot);
387
+ if (!packageName)
388
+ return undefined;
389
+ const packageVersion = readPackageVersion(packageRoot);
390
+ if (!packageVersion)
391
+ return undefined;
392
+ const binarySha256 = computeBinarySha256(realEntry);
393
+ if (!binarySha256)
394
+ return undefined;
395
+ let packageFingerprint;
396
+ try {
397
+ packageFingerprint = computePackageFingerprint(packageRoot);
398
+ }
399
+ catch {
400
+ return undefined;
401
+ }
402
+ const isScript = isJavaScriptScript(realEntry);
403
+ const launch = isScript
404
+ ? { command: safeRealpath(process.execPath), argsPrefix: [realEntry] }
405
+ : { command: realEntry, argsPrefix: [] };
406
+ return {
407
+ schemaVersion: 1,
408
+ packageName,
409
+ binName: path.basename(realEntry),
410
+ requested: input.requested,
411
+ entry: path.resolve(input.entry),
412
+ realEntry,
413
+ launch,
414
+ binarySha256,
415
+ packageRoot,
416
+ packageVersion,
417
+ packageFingerprint,
418
+ resolvedAt,
419
+ };
420
+ }
421
+ /**
422
+ * Stable content anchor for a controller identity. Two identities are the same
423
+ * controller when their anchors are equal. Kept in sync with the Worker anchor
424
+ * so DAG-owned and Worker-owned identity facts are comparable.
425
+ */
426
+ export function controllerIdentityAnchor(identity) {
427
+ return JSON.stringify({
428
+ schemaVersion: identity.schemaVersion,
429
+ packageName: identity.packageName,
430
+ binName: identity.binName,
431
+ requested: identity.requested,
432
+ entry: identity.entry,
433
+ realEntry: identity.realEntry,
434
+ launch: identity.launch,
435
+ binarySha256: identity.binarySha256,
436
+ packageRoot: identity.packageRoot,
437
+ packageVersion: identity.packageVersion,
438
+ packageFingerprint: identity.packageFingerprint,
439
+ });
440
+ }
441
+ export function controllerIdentitiesMatch(left, right) {
442
+ return Boolean(left && right && controllerIdentityAnchor(left) === controllerIdentityAnchor(right));
443
+ }
444
+ /**
445
+ * Resolve the identity of the controller that is executing this process.
446
+ *
447
+ * Preference order:
448
+ * 1. The launched CLI entry (argv[1]) when it resolves inside the loop-agent
449
+ * package — this is the real controller binary in production.
450
+ * 2. The package that physically contains this module — always the loop-agent
451
+ * package, which keeps identity resolvable under tests and source runs.
452
+ *
453
+ * Returns `undefined` when no loop-agent-rooted identity can be built. Callers
454
+ * treat `undefined` as legacy-unpinned rather than fabricating an identity.
455
+ */
456
+ export function resolveRunningControllerIdentity(options) {
457
+ const argv = options?.argv ?? process.argv;
458
+ const entryArg = argv[1];
459
+ if (entryArg) {
460
+ try {
461
+ const abs = path.resolve(entryArg);
462
+ const root = findPackageRoot(path.dirname(abs));
463
+ if (root && readPackageName(root) === LOOP_AGENT_PACKAGE_NAME) {
464
+ const identity = buildControllerIdentityFromEntry({
465
+ requested: entryArg,
466
+ entry: abs,
467
+ ...(options?.now ? { now: options.now } : {}),
468
+ });
469
+ if (identity)
470
+ return identity;
471
+ }
472
+ }
473
+ catch {
474
+ // Fall back to the module location below.
475
+ }
476
+ }
477
+ try {
478
+ const moduleFile = fileURLToPath(import.meta.url);
479
+ return buildControllerIdentityFromEntry({
480
+ requested: entryArg ?? "loop-agent",
481
+ entry: moduleFile,
482
+ ...(options?.now ? { now: options.now } : {}),
483
+ });
484
+ }
485
+ catch {
486
+ return undefined;
487
+ }
488
+ }
@@ -11,6 +11,9 @@ export const taskKindSchema = z.enum([
11
11
  "standard",
12
12
  "feature-study",
13
13
  "frontend-implementation",
14
+ "backend-test",
15
+ "knowledge-sync",
16
+ "knowledge-graph-bootstrap",
14
17
  ]);
15
18
  export const referenceRepoConfigSchema = z.object({
16
19
  name: z.string().min(1),
@@ -61,8 +64,10 @@ const taskConfigObjectSchema = z.object({
61
64
  taskId: z.string(),
62
65
  title: z.string(),
63
66
  sourceFiles: z.array(z.string()),
64
- /** standard: 本仓库需求实现;feature-study: 参考外部代码特性并在目标仓库落地;frontend-implementation: 使用前端实现 DAG 模板 */
67
+ /** standard: 本仓库需求实现;feature-study: 参考外部代码特性并在目标仓库落地;frontend-implementation: 前端实现 DAG;backend-test: 后端测试 DAG;knowledge-sync: 最终验证后回写测试知识库 DAG;knowledge-graph-bootstrap: AI 辅助业务知识图谱初始化 */
65
68
  taskKind: taskKindSchema.optional().default("standard"),
69
+ /** Feature 目录 id(如 F-2026-004)。knowledge-sync 必填(也可从 hardConstraints/需求正文/taskId 解析);用于收窄 writeSet */
70
+ featureId: z.string().min(1).optional(),
66
71
  referenceRepos: z.array(referenceRepoConfigSchema).optional().default([]),
67
72
  referenceDocs: z.array(referenceDocConfigSchema).optional().default([]),
68
73
  referenceMaxFilesPerRepo: z.number().int().positive().optional(),
@@ -14,10 +14,15 @@ import { createCompositeProgressReporter } from "./observability/progress-compos
14
14
  import { createRoutedWorkerEventStore } from "./observability/event-store.js";
15
15
  import { buildGlobalSnapshot } from "./observability/read-model.js";
16
16
  import { createObserveServer } from "./observe/server.js";
17
- import { getTaskPoolRoot, prepareTaskPoolRetry } from "./pool/run-store.js";
17
+ import { diagnoseTaskPoolStates, formatDoctorHuman, loadOperatorMapping } from "./pool/doctor.js";
18
+ import { migrateTaskPoolStates } from "./pool/migrate-state.js";
19
+ import { getTaskPoolRoot, prepareTaskPoolRetry, readFeatureTaskPoolStates } from "./pool/run-store.js";
20
+ import { TaskPoolStateError } from "./pool/types.js";
18
21
  import { taskSpecSchema } from "./task-spec/schema.js";
19
22
  import { validateTaskSpec } from "./task-spec/validate.js";
20
23
  import { validateFeatureTaskGraph } from "./task-graph/validate.js";
24
+ import { taskGraphSpecSchema } from "./task-graph/task-graph-schema.js";
25
+ import { planReadyTasks } from "./task-graph/ready-planner.js";
21
26
  import { renderFeatureReview, reviewFeature } from "./feature/review.js";
22
27
  import { renderFeatureRun, runFeature } from "./feature/run.js";
23
28
  import { draftFollowUpDecision } from "./follow-up/factory.js";
@@ -40,6 +45,58 @@ export function buildAgentWorkerProgram() {
40
45
  const report = program.command("report").description("Task Pool reporting utilities");
41
46
  const observe = program.command("observe").description("Observe UI server and snapshot utilities");
42
47
  const feature = program.command("feature").description("Feature-level review and delivery workflow");
48
+ const pool = program.command("pool").description("Task Pool state doctor and migration utilities");
49
+ pool
50
+ .command("doctor")
51
+ .requiredOption("--repo <repo-root>", "Target repo root")
52
+ .option("--json", "Emit stable JSON instead of the human summary")
53
+ .option("--mapping <file>", "Optional operator mapping JSON (taskId -> featureId)")
54
+ .description("Inventory legacy and v2 Task Pool states (read-only)")
55
+ .action(async (options) => {
56
+ const repoRoot = path.resolve(options.repo);
57
+ const mapping = await loadOperatorMapping(options.mapping);
58
+ const report = await diagnoseTaskPoolStates(repoRoot, { mapping });
59
+ process.stdout.write(options.json ? `${JSON.stringify(report, null, 2)}\n` : formatDoctorHuman(report));
60
+ });
61
+ pool
62
+ .command("migrate-state")
63
+ .requiredOption("--repo <repo-root>", "Target repo root")
64
+ .option("--dry-run", "Plan migration without writes (default when --apply is omitted)")
65
+ .option("--apply", "Apply migration (requires --owner and --reason)")
66
+ .option("--owner <owner>", "Migration owner for apply")
67
+ .option("--reason <reason>", "Why migration is being applied")
68
+ .option("--mapping <file>", "Optional operator mapping JSON (taskId -> featureId)")
69
+ .option("--json", "Emit stable JSON")
70
+ .description("Migrate legacy flat Task Pool states to feature-scoped v2 (default dry-run)")
71
+ .action(async (options) => {
72
+ try {
73
+ const result = await migrateTaskPoolStates({
74
+ repoRoot: path.resolve(options.repo),
75
+ apply: options.apply === true,
76
+ ...(options.owner ? { owner: options.owner } : {}),
77
+ ...(options.reason ? { reason: options.reason } : {}),
78
+ ...(options.mapping ? { mappingPath: options.mapping } : {}),
79
+ });
80
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
81
+ if (result.status === "failed")
82
+ process.exitCode = 1;
83
+ }
84
+ catch (error) {
85
+ const detail = error instanceof Error ? error.message : String(error);
86
+ const code = error instanceof TaskPoolStateError ? error.code : "task-pool-migrate-failed";
87
+ if (options.json) {
88
+ process.stdout.write(`${JSON.stringify({
89
+ schemaVersion: 1,
90
+ status: "failed",
91
+ error: { code, message: detail },
92
+ }, null, 2)}\n`);
93
+ }
94
+ else {
95
+ process.stderr.write(`${detail}\n`);
96
+ }
97
+ process.exitCode = 1;
98
+ }
99
+ });
43
100
  feature
44
101
  .command("review")
45
102
  .requiredOption("--feature-dir <dir>", "Feature directory containing acceptance.yaml and tasks/")
@@ -206,12 +263,14 @@ export function buildAgentWorkerProgram() {
206
263
  .command("retry")
207
264
  .argument("<task-id>", "Failed Task Pool task to requeue")
208
265
  .requiredOption("--repo <repo-root>", "Target repo root")
266
+ .option("--feature-id <feature-id>", "Canonical Feature id for the Task Pool state (required when identity is ambiguous)")
209
267
  .option("--reason <reason>", "Why the failure is safe to retry")
210
268
  .description("Explicitly requeue a failed task; the next run-ready uses a new workerRunId")
211
269
  .action(async (taskId, options) => {
212
270
  const result = await prepareTaskPoolRetry({
213
271
  repoRoot: path.resolve(options.repo),
214
272
  taskId,
273
+ ...(options.featureId ? { featureId: options.featureId } : {}),
215
274
  ...(options.reason ? { reason: options.reason } : {}),
216
275
  });
217
276
  process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
@@ -268,6 +327,42 @@ export function buildAgentWorkerProgram() {
268
327
  handleFollowUpCliError(error, options.json ?? false, "approve-followup");
269
328
  }
270
329
  });
330
+ batch
331
+ .command("plan-ready")
332
+ .requiredOption("--feature-dir <dir>", "Feature directory containing tasks/task-graph.yaml")
333
+ .requiredOption("--repo <repo-root>", "Target repo root")
334
+ .option("--limit <count>", "Maximum ready tasks to select")
335
+ .option("--json", "Emit stable JSON (default human summary)")
336
+ .description("Preview the read-only priority-aware ready plan without writing state or artifacts")
337
+ .action(async (options) => {
338
+ // Strict zero-write preview: load graph + specs + feature-scoped states only.
339
+ const featureDir = path.resolve(options.featureDir);
340
+ const repoRoot = path.resolve(options.repo);
341
+ const graph = taskGraphSpecSchema.parse(YAML.parse(await readFile(path.join(featureDir, "tasks", "task-graph.yaml"), "utf-8")));
342
+ const taskSpecs = new Map();
343
+ for (const node of graph.nodes) {
344
+ const taskFile = node.task ?? `${node.id}.yaml`;
345
+ taskSpecs.set(node.id, taskSpecSchema.parse(YAML.parse(await readFile(path.join(featureDir, "tasks", taskFile), "utf-8"))));
346
+ }
347
+ const selectionLimit = options.limit
348
+ ? Number.parseInt(options.limit, 10)
349
+ : graph.nodes.length;
350
+ if (!Number.isInteger(selectionLimit) || selectionLimit < 1) {
351
+ process.stderr.write("selectionLimit must be a positive integer\n");
352
+ process.exitCode = 1;
353
+ return;
354
+ }
355
+ const plan = planReadyTasks({
356
+ featureId: graph.feature_id,
357
+ graph,
358
+ taskSpecs,
359
+ states: await readFeatureTaskPoolStates(repoRoot, graph.feature_id),
360
+ selectionLimit,
361
+ });
362
+ process.stdout.write(options.json
363
+ ? `${JSON.stringify(plan, null, 2)}\n`
364
+ : `Selected: ${plan.selected.map((task) => task.taskId).join(", ") || "none"}\nDeferred: ${plan.deferred.map((task) => task.taskId).join(", ") || "none"}\nBlocked: ${plan.blocked.length}\n`);
365
+ });
271
366
  batch
272
367
  .command("run-ready")
273
368
  .requiredOption("--feature-dir <dir>", "Feature directory containing tasks/task-graph.yaml")
@@ -326,7 +421,8 @@ export function buildAgentWorkerProgram() {
326
421
  .command("serve")
327
422
  .requiredOption("--repo <repo-root>", "Target repo root")
328
423
  .option("--port <port>", "HTTP port", "8787")
329
- .option("--host <host>", "Bind host", "127.0.0.1")
424
+ .option("--host <host>", "Bind host (default 127.0.0.1; use 0.0.0.0 for LAN, no auth)", "127.0.0.1")
425
+ .option("--debug", "Log request source/method/path/status to stderr")
330
426
  .description("Start read-only observe HTTP server")
331
427
  .action(async (options) => {
332
428
  const repoRoot = path.resolve(options.repo);
@@ -334,6 +430,7 @@ export function buildAgentWorkerProgram() {
334
430
  repoRoot,
335
431
  host: options.host,
336
432
  port: Number.parseInt(options.port, 10),
433
+ debug: options.debug === true,
337
434
  });
338
435
  process.stdout.write(`${server.url}\n`);
339
436
  await new Promise((resolve) => {
@@ -6,7 +6,7 @@ import { promisify } from "node:util";
6
6
  import YAML from "yaml";
7
7
  import { z } from "zod";
8
8
  import { controllerIdentitiesMatch, LOOP_AGENT_PACKAGE_NAME, } from "../loop-agent/loop-agent-client.js";
9
- import { getRunsJsonlPath, getTaskPoolRoot, readAllTaskPoolStates, readJsonlFile } from "../pool/run-store.js";
9
+ import { getRunsJsonlPath, getTaskPoolRoot, readFeatureTaskPoolStates, readJsonlFile } from "../pool/run-store.js";
10
10
  import { acceptanceSpecSchema } from "../task-graph/acceptance-schema.js";
11
11
  import { taskGraphSpecSchema } from "../task-graph/task-graph-schema.js";
12
12
  import { validateFeatureTaskGraph } from "../task-graph/validate.js";
@@ -104,7 +104,7 @@ export async function prepareFeatureDelivery(input) {
104
104
  }
105
105
  const acceptance = acceptanceSpecSchema.parse(YAML.parse(await readFile(path.join(featureDir, "acceptance.yaml"), "utf-8")));
106
106
  const graph = taskGraphSpecSchema.parse(YAML.parse(await readFile(path.join(featureDir, "tasks", "task-graph.yaml"), "utf-8")));
107
- const states = await readAllTaskPoolStates(repoRoot);
107
+ const states = await readFeatureTaskPoolStates(repoRoot, featureId);
108
108
  const runs = await readJsonlFile(getRunsJsonlPath(repoRoot));
109
109
  const taskSpecs = new Map();
110
110
  for (const node of graph.nodes)
@@ -208,7 +208,7 @@ export async function validateDeliveryPackage(input) {
208
208
  blockers.push(`Git worktree is not clean for Closeout: ${closeoutStatus}`);
209
209
  const graph = taskGraphSpecSchema.parse(YAML.parse(await readFile(path.join(featureDir, "tasks", "task-graph.yaml"), "utf-8")));
210
210
  const acceptance = acceptanceSpecSchema.parse(YAML.parse(await readFile(path.join(featureDir, "acceptance.yaml"), "utf-8")));
211
- const states = await readAllTaskPoolStates(repoRoot);
211
+ const states = await readFeatureTaskPoolStates(repoRoot, featureId);
212
212
  const specs = new Map();
213
213
  for (const node of graph.nodes)
214
214
  specs.set(node.id, taskSpecSchema.parse(YAML.parse(await readFile(path.join(featureDir, "tasks", node.task), "utf-8"))));
@@ -46,16 +46,47 @@ async function auditTaskPoolFacts(repoRoot) {
46
46
  }
47
47
  const stateDir = path.join(getTaskPoolRoot(repoRoot), "states");
48
48
  try {
49
- for (const entry of await readdir(stateDir))
50
- if (entry.endsWith(".json"))
49
+ for (const entry of await readdir(stateDir, { withFileTypes: true })) {
50
+ if (entry.isFile() && entry.name.endsWith(".json")) {
51
51
  try {
52
- const parsed = taskPoolStateFactSchema.safeParse(JSON.parse(await readFile(path.join(stateDir, entry), "utf-8")));
53
- if (!parsed.success || parsed.data.taskId !== entry.slice(0, -5))
54
- warnings.push(`Task Pool state is semantically invalid: ${entry}`);
52
+ const parsed = taskPoolStateFactSchema.safeParse(JSON.parse(await readFile(path.join(stateDir, entry.name), "utf-8")));
53
+ if (!parsed.success || parsed.data.taskId !== entry.name.slice(0, -5)) {
54
+ warnings.push(`Task Pool state is semantically invalid: ${entry.name}`);
55
+ }
56
+ else {
57
+ warnings.push(`legacy Task Pool state requires migration: ${entry.name}`);
58
+ }
55
59
  }
56
60
  catch {
57
- warnings.push(`Task Pool state is corrupt: ${entry}`);
61
+ warnings.push(`Task Pool state is corrupt: ${entry.name}`);
58
62
  }
63
+ continue;
64
+ }
65
+ if (!entry.isDirectory())
66
+ continue;
67
+ const featureId = entry.name;
68
+ try {
69
+ for (const stateName of await readdir(path.join(stateDir, featureId))) {
70
+ if (!stateName.endsWith(".json"))
71
+ continue;
72
+ const relative = `${featureId}/${stateName}`;
73
+ try {
74
+ const parsed = taskPoolStateFactSchema.safeParse(JSON.parse(await readFile(path.join(stateDir, featureId, stateName), "utf-8")));
75
+ const expectedTaskId = stateName.slice(0, -5);
76
+ if (!parsed.success || parsed.data.taskId !== expectedTaskId || parsed.data.featureId !== featureId) {
77
+ warnings.push(`Task Pool state is semantically invalid: ${relative}`);
78
+ }
79
+ }
80
+ catch {
81
+ warnings.push(`Task Pool state is corrupt: ${relative}`);
82
+ }
83
+ }
84
+ }
85
+ catch (error) {
86
+ if (!isNotFound(error))
87
+ warnings.push(`Task Pool states for ${featureId} are unreadable: ${message(error)}`);
88
+ }
89
+ }
59
90
  }
60
91
  catch (error) {
61
92
  if (!isNotFound(error))
@@ -35,7 +35,7 @@ export function projectNextAction(input, status) {
35
35
  return {
36
36
  kind: "retry_task",
37
37
  label: `重试环境失败任务 ${failed.taskId}`,
38
- command: `agent-worker task retry ${quote(failed.taskId)} --repo ${quote(input.repoRoot)}`,
38
+ command: `agent-worker task retry ${quote(failed.taskId)} --feature-id ${quote(input.featureId)} --repo ${quote(input.repoRoot)}`,
39
39
  };
40
40
  }
41
41
  return {
@@ -51,9 +51,17 @@ export function projectNextAction(input, status) {
51
51
  };
52
52
  }
53
53
  if (status === "ready") {
54
+ const selected = input.planning?.selected[0];
55
+ const deferredCount = input.planning?.deferred.length ?? 0;
56
+ const blockedCount = input.planning?.blocked.length ?? 0;
57
+ const reasonSuffix = selected && (deferredCount > 0 || blockedCount > 0)
58
+ ? `;deferred ${deferredCount} / blocked ${blockedCount}`
59
+ : "";
54
60
  return {
55
61
  kind: "run_feature",
56
- label: "执行当前 Ready tasks",
62
+ label: selected
63
+ ? `执行最高优先级 Ready task ${selected.taskId} (${selected.priority})${reasonSuffix}`
64
+ : "执行当前 Ready tasks",
57
65
  command: `agent-worker batch run-ready ${featureArgs}`,
58
66
  };
59
67
  }