@tea-agent/loop-agent 0.24.11-beta.0 → 0.25.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 (97) hide show
  1. package/CHANGELOG.md +53 -4
  2. package/README.md +4 -4
  3. package/dist/application/dag/generate-task-dag.js +25 -8
  4. package/dist/commands/dag-init-hybrid.js +3 -2
  5. package/dist/commands/delegate.js +11 -11
  6. package/dist/commands/init.js +29 -14
  7. package/dist/commands/instructions.js +1 -2
  8. package/dist/executors/dag-pi-executor.js +74 -13
  9. package/dist/executors/pi-executor.js +25 -7
  10. package/dist/executors/pi-prompt-transport.js +198 -0
  11. package/dist/executors/pi-sdk-executor.js +1 -1
  12. package/dist/executors/process-tree.js +33 -0
  13. package/dist/executors/shell-executor.js +362 -24
  14. package/dist/executors/shell-verification.js +4 -2
  15. package/dist/infrastructure/harness/task-store.js +31 -0
  16. package/dist/shared/operator/capabilities.js +6 -0
  17. package/dist/task/read-model.js +3 -7
  18. package/dist/task/runtime.js +12 -3
  19. package/dist/verification/maven/cache.js +142 -0
  20. package/dist/verification/maven/index.js +120 -0
  21. package/dist/verification/maven/plan-commands.js +421 -0
  22. package/dist/verification/maven/pom-static.js +136 -0
  23. package/dist/verification/maven/scope-resolve.js +153 -0
  24. package/dist/verification/maven/stale.js +130 -0
  25. package/dist/verification/maven/types.js +23 -0
  26. package/dist/verification/maven/workspace-graph.js +322 -0
  27. package/dist/worker/cli.js +236 -21
  28. package/dist/worker/delivery/final-verification.js +12 -0
  29. package/dist/worker/delivery/git-transaction.js +32 -7
  30. package/dist/worker/delivery/package.js +9 -5
  31. package/dist/worker/delivery/verification-bundle.js +26 -5
  32. package/dist/worker/feature/advance.js +301 -0
  33. package/dist/worker/feature/doctor.js +223 -0
  34. package/dist/worker/feature/next-action.js +11 -3
  35. package/dist/worker/feature/scaffold.js +798 -0
  36. package/dist/worker/observe/dag-run-artifacts.js +90 -0
  37. package/dist/worker/observe/node-input.js +444 -0
  38. package/dist/worker/observe/routes.js +17 -0
  39. package/dist/worker/observe/static/api.js +9 -0
  40. package/dist/worker/observe/static/constants.js +9 -0
  41. package/dist/worker/observe/static/state.js +14 -0
  42. package/dist/worker/observe/static/styles.css +74 -0
  43. package/dist/worker/observe/static/views/dag-inspector.js +371 -15
  44. package/dist/worker/outcomes/projector.js +5 -1
  45. package/dist/worker/run-task/run-task.js +18 -2
  46. package/dist/worker/runner/run-ready.js +41 -1
  47. package/dist/workflows/dag/backend-test-markdown-workflow.js +300 -9
  48. package/dist/workflows/dag/backend-test-result-contract.js +103 -0
  49. package/dist/workflows/dag/failure-category.js +5 -1
  50. package/dist/workflows/dag/frontend-implementation-contract.js +4 -0
  51. package/dist/workflows/dag/frontend-prewrite-gate.js +0 -17
  52. package/dist/workflows/dag/frontend-test-result-contract.js +106 -41
  53. package/dist/workflows/dag/init-hybrid.js +224 -39
  54. package/dist/workflows/dag/node-execution.js +3 -2
  55. package/dist/workflows/dag/reconcile-run.js +24 -0
  56. package/dist/workflows/dag/types.js +57 -0
  57. package/dist/workflows/dag/validate.js +19 -2
  58. package/dist/workflows/loop/actions/dag-action.js +2 -1
  59. package/docs/architecture/dag-execution.md +5 -1
  60. package/docs/architecture/runtime-boundaries.md +11 -1
  61. package/docs/architecture/worker-and-feature.md +2 -0
  62. package/docs/init-surface.manifest.json +3 -0
  63. package/docs/templates/backend-test-dag.json +3 -3
  64. package/docs/templates/product-line/README.md +17 -1
  65. package/docs/templates/product-line/feature-scaffold-batch.example.yaml +31 -0
  66. package/docs/templates/product-line/scaffold-samples/README.md +36 -0
  67. package/docs/templates/product-line/scaffold-samples/backend-only/acceptance.yaml +13 -0
  68. package/docs/templates/product-line/scaffold-samples/backend-only/design.md +14 -0
  69. package/docs/templates/product-line/scaffold-samples/backend-only/feature.yaml +3 -0
  70. package/docs/templates/product-line/scaffold-samples/backend-only/requirement.md +6 -0
  71. package/docs/templates/product-line/scaffold-samples/backend-only/tasks/BE-IMPL-001.yaml +83 -0
  72. package/docs/templates/product-line/scaffold-samples/backend-only/tasks/task-graph.yaml +14 -0
  73. package/docs/templates/product-line/scaffold-samples/fe-with-api/acceptance.yaml +15 -0
  74. package/docs/templates/product-line/scaffold-samples/fe-with-api/design.md +18 -0
  75. package/docs/templates/product-line/scaffold-samples/fe-with-api/feature.yaml +3 -0
  76. package/docs/templates/product-line/scaffold-samples/fe-with-api/requirement.md +6 -0
  77. package/docs/templates/product-line/scaffold-samples/fe-with-api/tasks/BE-IMPL-001.yaml +84 -0
  78. package/docs/templates/product-line/scaffold-samples/fe-with-api/tasks/CONTRACT-001.yaml +84 -0
  79. package/docs/templates/product-line/scaffold-samples/fe-with-api/tasks/FE-IMPL-001.yaml +86 -0
  80. package/docs/templates/product-line/scaffold-samples/fe-with-api/tasks/task-graph.yaml +40 -0
  81. package/docs/templates/product-line/scaffold-samples/frontend-only/acceptance.yaml +13 -0
  82. package/docs/templates/product-line/scaffold-samples/frontend-only/design.md +14 -0
  83. package/docs/templates/product-line/scaffold-samples/frontend-only/feature.yaml +3 -0
  84. package/docs/templates/product-line/scaffold-samples/frontend-only/requirement.md +6 -0
  85. package/docs/templates/product-line/scaffold-samples/frontend-only/tasks/FE-IMPL-001.yaml +85 -0
  86. package/docs/templates/product-line/scaffold-samples/frontend-only/tasks/task-graph.yaml +14 -0
  87. package/examples/l5-report-coms-process-definition.html +322 -0
  88. package/package.json +1 -1
  89. package/skills/agent-worker/references/agent-worker-operator.md +84 -0
  90. package/skills/frontend-implementation/references/node-contracts.md +2 -2
  91. package/skills/loop-agent/SKILL.md +14 -16
  92. package/skills/loop-agent/references/command-reference.md +33 -12
  93. package/skills/loop-agent/references/harness-policy.md +28 -6
  94. package/skills/loop-agent/references/hybrid-dag.md +8 -7
  95. package/skills/loop-agent/references/post-implementation-and-patterns.md +19 -9
  96. package/skills/loop-agent/references/source-and-plan-practice.md +161 -0
  97. package/skills/loop-agent/references/task-workflow.md +13 -5
@@ -68,6 +68,8 @@ export function getTaskPaths(repoRoot, taskId) {
68
68
  contractPath: path.join(taskDir, 'contract.json'),
69
69
  /** Journaled contract transactions directory. */
70
70
  transactionsDir: path.join(taskDir, '.transactions'),
71
+ /** Canonical current DAG draft input for validate / run-dag. */
72
+ dagDraftPath: path.join(taskDir, 'dag.json'),
71
73
  };
72
74
  }
73
75
  export async function createTask(repoRoot, taskId, title) {
@@ -177,8 +179,15 @@ export async function getTaskStatus(repoRoot, taskId) {
177
179
  return loadWorkflowState(getTaskPaths(repoRoot, taskId).statePath);
178
180
  }
179
181
  export async function loadTaskConfig(repoRoot, taskId) {
180
- const raw = await readFile(getTaskPaths(repoRoot, taskId).taskConfigPath, 'utf-8');
181
- return parseTaskConfigForRuntime(JSON.parse(raw));
182
+ const configPath = getTaskPaths(repoRoot, taskId).taskConfigPath;
183
+ const raw = await readFile(configPath, 'utf-8');
184
+ try {
185
+ return parseTaskConfigForRuntime(JSON.parse(raw));
186
+ }
187
+ catch (error) {
188
+ const message = error instanceof Error ? error.message : String(error);
189
+ throw new Error(`invalid task config at ${configPath}: ${message}`);
190
+ }
182
191
  }
183
192
  export function parseTaskConfigForRuntime(rawConfig) {
184
193
  const parsed = taskConfigSchema.parse(rawConfig);
@@ -292,7 +301,7 @@ export function getSupportedNextCommand(step, taskId, status, options) {
292
301
  return `npm run dev -- loop status ${taskId}`;
293
302
  }
294
303
  if (step === 'analyze' || step === 'plan' || step === 'spec' || step === 'implement' || step === 'verify' || step === 'retrospective') {
295
- return `npm run dev -- dag run-task ${taskId} --profile auto --strict-models --output /tmp/${taskId}-dag.json`;
304
+ return `npm run dev -- dag run-task ${taskId} --profile auto --strict-models`;
296
305
  }
297
306
  return `${step} step not yet implemented in this prototype; human intervention required`;
298
307
  }
@@ -0,0 +1,142 @@
1
+ import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync, } from "node:fs";
2
+ import path from "node:path";
3
+ import { MAVEN_CACHE_SCHEMA_VERSION, MAVEN_PLANNER_VERSION, } from "./types.js";
4
+ import { buildMavenWorkspaceGraph } from "./workspace-graph.js";
5
+ const CACHE_FILE_NAME = "maven-workspace-graph.json";
6
+ /** Directory names ignored during discovery and lightweight entry-list checks. */
7
+ const SKIP_DIR_NAMES = new Set([
8
+ ".git",
9
+ ".harness",
10
+ "node_modules",
11
+ "target",
12
+ "dist",
13
+ "build",
14
+ ".idea",
15
+ ".vscode",
16
+ "coverage",
17
+ ".turbo",
18
+ ".next",
19
+ ]);
20
+ export function defaultMavenCacheDir(repoRoot) {
21
+ return path.join(repoRoot, ".harness", "cache");
22
+ }
23
+ export function mavenGraphCachePath(cacheDir) {
24
+ return path.join(cacheDir, CACHE_FILE_NAME);
25
+ }
26
+ function isValidCachedGraph(value) {
27
+ if (!value || typeof value !== "object")
28
+ return false;
29
+ const graph = value;
30
+ return (graph.schemaVersion === MAVEN_CACHE_SCHEMA_VERSION &&
31
+ graph.plannerVersion === MAVEN_PLANNER_VERSION &&
32
+ typeof graph.fingerprint === "string" &&
33
+ Array.isArray(graph.pomPaths) &&
34
+ Array.isArray(graph.nodes));
35
+ }
36
+ /**
37
+ * Lightweight freshness probe: only stat previously recorded directories and
38
+ * file metadata. Does NOT re-walk the whole tree or re-parse POMs.
39
+ * Returns true when the cached graph can be reused as-is.
40
+ */
41
+ export function isCachedGraphStillFresh(repoRoot, graph) {
42
+ if (graph.schemaVersion !== MAVEN_CACHE_SCHEMA_VERSION ||
43
+ graph.plannerVersion !== MAVEN_PLANNER_VERSION) {
44
+ return false;
45
+ }
46
+ const manifest = graph.manifest;
47
+ if (!manifest) {
48
+ // Old cache shape without manifest cannot prove lightweight freshness.
49
+ return false;
50
+ }
51
+ // Directory entry lists: any add/remove invalidates.
52
+ for (const dir of manifest.directories) {
53
+ const absDir = dir.path === "" ? repoRoot : path.join(repoRoot, ...dir.path.split("/"));
54
+ let entries;
55
+ try {
56
+ entries = readdirSync(absDir, { withFileTypes: true })
57
+ .filter((e) => {
58
+ if (SKIP_DIR_NAMES.has(e.name))
59
+ return false;
60
+ try {
61
+ return !e.isSymbolicLink();
62
+ }
63
+ catch {
64
+ return true;
65
+ }
66
+ })
67
+ .map((e) => e.name)
68
+ .sort();
69
+ }
70
+ catch {
71
+ return false;
72
+ }
73
+ if (entries.length !== dir.entryNames.length)
74
+ return false;
75
+ for (let i = 0; i < entries.length; i += 1) {
76
+ if (entries[i] !== dir.entryNames[i])
77
+ return false;
78
+ }
79
+ }
80
+ // File metadata: size/mtime change triggers rebuild (content re-hash on rebuild).
81
+ for (const file of manifest.files) {
82
+ const abs = path.join(repoRoot, ...file.path.split("/"));
83
+ try {
84
+ const lst = lstatSync(abs);
85
+ if (lst.isSymbolicLink())
86
+ return false;
87
+ const st = statSync(abs);
88
+ if (st.size !== file.size)
89
+ return false;
90
+ if (st.mtimeMs !== file.mtimeMs)
91
+ return false;
92
+ }
93
+ catch {
94
+ return false;
95
+ }
96
+ }
97
+ return true;
98
+ }
99
+ /**
100
+ * Load cached graph when schema/planner versions match and lightweight
101
+ * directory/file metadata is unchanged. Full parse only on miss/stale.
102
+ */
103
+ export function loadOrBuildMavenWorkspaceGraph(input) {
104
+ const cacheDir = input.cacheDir ?? defaultMavenCacheDir(input.repoRoot);
105
+ const cachePath = mavenGraphCachePath(cacheDir);
106
+ if (existsSync(cachePath)) {
107
+ try {
108
+ const raw = JSON.parse(readFileSync(cachePath, "utf8"));
109
+ if (isValidCachedGraph(raw) && isCachedGraphStillFresh(input.repoRoot, raw)) {
110
+ return raw;
111
+ }
112
+ }
113
+ catch {
114
+ // Fall through to rebuild.
115
+ }
116
+ }
117
+ const graph = buildMavenWorkspaceGraph(input.repoRoot);
118
+ writeGraphCache(cacheDir, graph);
119
+ return graph;
120
+ }
121
+ export function writeGraphCache(cacheDir, graph) {
122
+ mkdirSync(cacheDir, { recursive: true });
123
+ const payload = `${JSON.stringify(graph, null, 2)}\n`;
124
+ // Guard: never persist absolute host paths.
125
+ if (/[A-Za-z]:[\\/]/.test(payload) || payload.includes("\\\\")) {
126
+ // Absolute Windows paths are forbidden; re-check using graph fields only.
127
+ const serialized = JSON.stringify({
128
+ schemaVersion: graph.schemaVersion,
129
+ plannerVersion: graph.plannerVersion,
130
+ fingerprint: graph.fingerprint,
131
+ pomPaths: graph.pomPaths,
132
+ wrapperPaths: graph.wrapperPaths,
133
+ nodes: graph.nodes,
134
+ reactors: graph.reactors,
135
+ reactorRootByPomPath: graph.reactorRootByPomPath,
136
+ manifest: graph.manifest,
137
+ });
138
+ writeFileSync(mavenGraphCachePath(cacheDir), `${serialized}\n`, "utf8");
139
+ return;
140
+ }
141
+ writeFileSync(mavenGraphCachePath(cacheDir), payload, "utf8");
142
+ }
@@ -0,0 +1,120 @@
1
+ import path from "node:path";
2
+ import { loadOrBuildMavenWorkspaceGraph } from "./cache.js";
3
+ import { commandContainsMaven, extractExplicitMavenFile, parseMavenInvocation, planMavenCommands, } from "./plan-commands.js";
4
+ import { resolveMavenTargets } from "./scope-resolve.js";
5
+ import { assertMavenPlanFresh } from "./stale.js";
6
+ import { MAVEN_CACHE_SCHEMA_VERSION, MAVEN_PLANNER_VERSION, MavenPlanError, MavenPlanStaleError, } from "./types.js";
7
+ export { MAVEN_CACHE_SCHEMA_VERSION, MAVEN_PLANNER_VERSION, MavenPlanError, MavenPlanStaleError, assertMavenPlanFresh, };
8
+ function toPosix(value) {
9
+ return value.replace(/\\/g, "/");
10
+ }
11
+ function allMavenCommandsHaveExplicitFile(commands) {
12
+ const mavenCommands = commands.filter(commandContainsMaven);
13
+ if (mavenCommands.length === 0)
14
+ return false;
15
+ return mavenCommands.every((command) => {
16
+ const parsed = parseMavenInvocation(command);
17
+ if (!parsed)
18
+ return false;
19
+ return extractExplicitMavenFile(parsed.mavenArgs) !== undefined;
20
+ });
21
+ }
22
+ function targetsFromExplicitFiles(input) {
23
+ const byPom = new Map();
24
+ for (const command of input.commands) {
25
+ const parsed = parseMavenInvocation(command);
26
+ if (!parsed)
27
+ continue;
28
+ const explicit = extractExplicitMavenFile(parsed.mavenArgs);
29
+ if (!explicit)
30
+ continue;
31
+ // Resolve relative -f against the original command cwd.
32
+ const baseCwd = command.cwd ?? input.repoRoot;
33
+ const absolute = path.isAbsolute(explicit)
34
+ ? path.normalize(explicit)
35
+ : path.resolve(baseCwd, explicit);
36
+ const rel = toPosix(path.relative(input.repoRoot, absolute));
37
+ if (!rel || rel.startsWith("..") || path.isAbsolute(rel)) {
38
+ // Keep unresolved path for planMavenCommands validation/fail-fast.
39
+ byPom.set(toPosix(explicit), {
40
+ pomPath: toPosix(explicit),
41
+ artifactId: path.posix.basename(toPosix(explicit).replace(/\/pom\.xml$/i, "")) || "unknown",
42
+ });
43
+ continue;
44
+ }
45
+ const node = input.graph.nodes.find((n) => n.pomPath === rel);
46
+ if (node) {
47
+ const reactorRoot = input.graph.reactorRootByPomPath[node.pomPath];
48
+ if (reactorRoot && reactorRoot !== node.pomPath) {
49
+ const reactor = input.graph.reactors.find((r) => r.rootPomPath === reactorRoot);
50
+ byPom.set(node.pomPath, {
51
+ pomPath: node.pomPath,
52
+ artifactId: node.artifactId,
53
+ reactorRootPomPath: reactorRoot,
54
+ moduleCoordinate: reactor?.modulePathByPomPath?.[node.pomPath] ??
55
+ reactor?.artifactIdByPomPath[node.pomPath] ??
56
+ node.artifactId,
57
+ });
58
+ }
59
+ else {
60
+ byPom.set(node.pomPath, {
61
+ pomPath: node.pomPath,
62
+ artifactId: node.artifactId,
63
+ reactorRootPomPath: reactorRoot === node.pomPath ? reactorRoot : undefined,
64
+ });
65
+ }
66
+ }
67
+ else {
68
+ const base = rel.replace(/\/pom\.xml$/i, "");
69
+ const artifactId = base.split("/").filter(Boolean).pop() ?? rel;
70
+ byPom.set(rel, { pomPath: rel, artifactId });
71
+ }
72
+ }
73
+ return [...byPom.values()];
74
+ }
75
+ /**
76
+ * Plan Maven verification commands from task scope + static workspace graph.
77
+ * Non-Maven commands are returned unchanged. Returns plan=null when no Maven
78
+ * commands are present (REQ-MVN-007).
79
+ */
80
+ export function planMavenVerification(input) {
81
+ const mavenCommands = input.commands.filter(commandContainsMaven);
82
+ if (mavenCommands.length === 0) {
83
+ return { commands: input.commands, plan: null };
84
+ }
85
+ const graph = loadOrBuildMavenWorkspaceGraph({
86
+ repoRoot: input.repoRoot,
87
+ cacheDir: input.cacheDir,
88
+ });
89
+ if (graph.pomPaths.length === 0) {
90
+ throw new MavenPlanError("Maven verification is requested but no pom.xml was found in the repository.", { code: "maven-no-pom" });
91
+ }
92
+ // When every Maven command already carries a resolvable -f, skip automatic
93
+ // scope target resolution (wide/empty scope must not block explicit targets).
94
+ const targets = allMavenCommandsHaveExplicitFile(input.commands)
95
+ ? targetsFromExplicitFiles({
96
+ repoRoot: input.repoRoot,
97
+ commands: input.commands,
98
+ graph,
99
+ })
100
+ : resolveMavenTargets({
101
+ graph,
102
+ scope: input.scope,
103
+ });
104
+ const planned = planMavenCommands({
105
+ repoRoot: input.repoRoot,
106
+ commands: input.commands,
107
+ targets,
108
+ graph,
109
+ scope: input.scope,
110
+ });
111
+ // Ensure planned commands keep original repoRoot cwd (explicit -f, not module cd).
112
+ const commands = planned.commands.map((command) => ({
113
+ ...command,
114
+ cwd: input.repoRoot,
115
+ }));
116
+ return {
117
+ commands,
118
+ plan: planned.plan,
119
+ };
120
+ }