@tea-agent/loop-agent 0.24.11-beta.0 → 0.25.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 (84) hide show
  1. package/CHANGELOG.md +28 -5
  2. package/dist/commands/init.js +16 -2
  3. package/dist/executors/dag-pi-executor.js +74 -13
  4. package/dist/executors/pi-executor.js +25 -7
  5. package/dist/executors/pi-prompt-transport.js +198 -0
  6. package/dist/executors/pi-sdk-executor.js +1 -1
  7. package/dist/executors/process-tree.js +33 -0
  8. package/dist/executors/shell-executor.js +332 -24
  9. package/dist/executors/shell-verification.js +4 -2
  10. package/dist/infrastructure/harness/task-store.js +31 -0
  11. package/dist/shared/operator/capabilities.js +6 -0
  12. package/dist/verification/maven/cache.js +142 -0
  13. package/dist/verification/maven/index.js +120 -0
  14. package/dist/verification/maven/plan-commands.js +421 -0
  15. package/dist/verification/maven/pom-static.js +136 -0
  16. package/dist/verification/maven/scope-resolve.js +153 -0
  17. package/dist/verification/maven/stale.js +130 -0
  18. package/dist/verification/maven/types.js +23 -0
  19. package/dist/verification/maven/workspace-graph.js +322 -0
  20. package/dist/worker/cli.js +236 -21
  21. package/dist/worker/delivery/final-verification.js +12 -0
  22. package/dist/worker/delivery/git-transaction.js +32 -7
  23. package/dist/worker/delivery/package.js +9 -5
  24. package/dist/worker/delivery/verification-bundle.js +26 -5
  25. package/dist/worker/feature/advance.js +301 -0
  26. package/dist/worker/feature/doctor.js +223 -0
  27. package/dist/worker/feature/next-action.js +11 -3
  28. package/dist/worker/feature/scaffold.js +798 -0
  29. package/dist/worker/observe/dag-run-artifacts.js +90 -0
  30. package/dist/worker/observe/node-input.js +444 -0
  31. package/dist/worker/observe/routes.js +17 -0
  32. package/dist/worker/observe/static/api.js +9 -0
  33. package/dist/worker/observe/static/constants.js +9 -0
  34. package/dist/worker/observe/static/state.js +14 -0
  35. package/dist/worker/observe/static/styles.css +74 -0
  36. package/dist/worker/observe/static/views/dag-inspector.js +371 -15
  37. package/dist/worker/outcomes/projector.js +5 -1
  38. package/dist/worker/runner/run-ready.js +41 -1
  39. package/dist/workflows/dag/backend-test-markdown-workflow.js +202 -9
  40. package/dist/workflows/dag/backend-test-result-contract.js +103 -0
  41. package/dist/workflows/dag/failure-category.js +5 -1
  42. package/dist/workflows/dag/frontend-implementation-contract.js +4 -0
  43. package/dist/workflows/dag/frontend-prewrite-gate.js +0 -17
  44. package/dist/workflows/dag/frontend-test-result-contract.js +106 -41
  45. package/dist/workflows/dag/init-hybrid.js +220 -32
  46. package/dist/workflows/dag/node-execution.js +3 -2
  47. package/dist/workflows/dag/reconcile-run.js +24 -0
  48. package/dist/workflows/dag/types.js +46 -0
  49. package/dist/workflows/dag/validate.js +19 -2
  50. package/docs/architecture/dag-execution.md +5 -1
  51. package/docs/architecture/runtime-boundaries.md +11 -1
  52. package/docs/architecture/worker-and-feature.md +2 -0
  53. package/docs/templates/backend-test-dag.json +3 -3
  54. package/docs/templates/product-line/README.md +17 -1
  55. package/docs/templates/product-line/feature-scaffold-batch.example.yaml +31 -0
  56. package/docs/templates/product-line/scaffold-samples/README.md +36 -0
  57. package/docs/templates/product-line/scaffold-samples/backend-only/acceptance.yaml +13 -0
  58. package/docs/templates/product-line/scaffold-samples/backend-only/design.md +14 -0
  59. package/docs/templates/product-line/scaffold-samples/backend-only/feature.yaml +3 -0
  60. package/docs/templates/product-line/scaffold-samples/backend-only/requirement.md +6 -0
  61. package/docs/templates/product-line/scaffold-samples/backend-only/tasks/BE-IMPL-001.yaml +83 -0
  62. package/docs/templates/product-line/scaffold-samples/backend-only/tasks/task-graph.yaml +14 -0
  63. package/docs/templates/product-line/scaffold-samples/fe-with-api/acceptance.yaml +15 -0
  64. package/docs/templates/product-line/scaffold-samples/fe-with-api/design.md +18 -0
  65. package/docs/templates/product-line/scaffold-samples/fe-with-api/feature.yaml +3 -0
  66. package/docs/templates/product-line/scaffold-samples/fe-with-api/requirement.md +6 -0
  67. package/docs/templates/product-line/scaffold-samples/fe-with-api/tasks/BE-IMPL-001.yaml +84 -0
  68. package/docs/templates/product-line/scaffold-samples/fe-with-api/tasks/CONTRACT-001.yaml +84 -0
  69. package/docs/templates/product-line/scaffold-samples/fe-with-api/tasks/FE-IMPL-001.yaml +86 -0
  70. package/docs/templates/product-line/scaffold-samples/fe-with-api/tasks/task-graph.yaml +40 -0
  71. package/docs/templates/product-line/scaffold-samples/frontend-only/acceptance.yaml +13 -0
  72. package/docs/templates/product-line/scaffold-samples/frontend-only/design.md +14 -0
  73. package/docs/templates/product-line/scaffold-samples/frontend-only/feature.yaml +3 -0
  74. package/docs/templates/product-line/scaffold-samples/frontend-only/requirement.md +6 -0
  75. package/docs/templates/product-line/scaffold-samples/frontend-only/tasks/FE-IMPL-001.yaml +85 -0
  76. package/docs/templates/product-line/scaffold-samples/frontend-only/tasks/task-graph.yaml +14 -0
  77. package/examples/l5-report-coms-process-definition.html +322 -0
  78. package/package.json +1 -1
  79. package/skills/agent-worker/references/agent-worker-operator.md +84 -0
  80. package/skills/frontend-implementation/references/node-contracts.md +2 -2
  81. package/skills/loop-agent/SKILL.md +8 -8
  82. package/skills/loop-agent/references/command-reference.md +14 -2
  83. package/skills/loop-agent/references/harness-policy.md +22 -0
  84. package/skills/loop-agent/references/task-workflow.md +5 -0
@@ -0,0 +1,136 @@
1
+ import { readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { MavenPlanError } from "./types.js";
4
+ function toPosix(value) {
5
+ return value.replace(/\\/g, "/");
6
+ }
7
+ function decodeXmlEntities(value) {
8
+ return value
9
+ .replace(/&lt;/g, "<")
10
+ .replace(/&gt;/g, ">")
11
+ .replace(/&amp;/g, "&")
12
+ .replace(/&quot;/g, '"')
13
+ .replace(/&apos;/g, "'");
14
+ }
15
+ function stripCommentsAndCdataNoise(xml) {
16
+ return xml
17
+ .replace(/<!--[\s\S]*?-->/g, "")
18
+ .replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, "$1");
19
+ }
20
+ function firstTagText(xml, tag) {
21
+ const re = new RegExp(`<${tag}(?:\\s[^>]*)?>([\\s\\S]*?)</${tag}>`, "i");
22
+ const match = xml.match(re);
23
+ if (!match)
24
+ return undefined;
25
+ const text = decodeXmlEntities(match[1] ?? "").trim();
26
+ return text.length > 0 ? text : undefined;
27
+ }
28
+ /**
29
+ * Extract text of a direct child tag under the root <project> element.
30
+ * Nested blocks such as <parent> are ignored so parent artifactId/groupId
31
+ * cannot shadow the project-level fields.
32
+ */
33
+ function projectDirectChildText(xml, tag) {
34
+ const projectMatch = xml.match(/<project(?:\s[^>]*)?>([\s\S]*)<\/project>/i);
35
+ const body = projectMatch?.[1] ?? xml;
36
+ // Strip nested element blocks so only project-level direct children remain.
37
+ const withoutNested = body.replace(/<(parent|dependency|plugin|profile|modules|dependencies|build|properties|reporting|repositories|pluginRepositories|developers|contributors|licenses|scm|organization|issueManagement|ciManagement|distributionManagement|dependencyManagement|profiles)(?:\s[^>]*)?>[\s\S]*?<\/\1>/gi, "");
38
+ return firstTagText(withoutNested, tag);
39
+ }
40
+ function collectModuleTexts(xml) {
41
+ const modulesBlock = xml.match(/<modules(?:\s[^>]*)?>([\s\S]*?)<\/modules>/i);
42
+ if (!modulesBlock)
43
+ return [];
44
+ const body = modulesBlock[1] ?? "";
45
+ const modules = [];
46
+ const re = /<module(?:\s[^>]*)?>([\s\S]*?)<\/module>/gi;
47
+ let match;
48
+ while ((match = re.exec(body)) !== null) {
49
+ const text = decodeXmlEntities(match[1] ?? "").trim();
50
+ if (!text)
51
+ continue;
52
+ // Reject property-style module paths that cannot be proven statically.
53
+ if (text.includes("${")) {
54
+ throw new MavenPlanError(`POM module path is not statically provable: ${text}`, { code: "maven-pom-unresolvable-module" });
55
+ }
56
+ modules.push(toPosix(text));
57
+ }
58
+ return modules;
59
+ }
60
+ function parentBlockBody(xml) {
61
+ const parentBlock = xml.match(/<parent(?:\s[^>]*)?>([\s\S]*?)<\/parent>/i);
62
+ return parentBlock?.[1];
63
+ }
64
+ function parentRelativePath(xml) {
65
+ const body = parentBlockBody(xml);
66
+ if (body === undefined)
67
+ return undefined;
68
+ const relative = firstTagText(body, "relativePath");
69
+ if (relative === undefined)
70
+ return undefined;
71
+ if (relative.includes("${")) {
72
+ throw new MavenPlanError(`POM parent relativePath is not statically provable: ${relative}`, { code: "maven-pom-unresolvable-parent" });
73
+ }
74
+ return toPosix(relative);
75
+ }
76
+ /**
77
+ * Parse only the static POM fields required to build reactor relationships.
78
+ * Does not execute Maven and does not resolve the effective POM.
79
+ */
80
+ export function parseStaticPomXml(xml, sourceLabel) {
81
+ const cleaned = stripCommentsAndCdataNoise(xml);
82
+ const artifactId = projectDirectChildText(cleaned, "artifactId");
83
+ if (!artifactId) {
84
+ throw new MavenPlanError(`POM is missing <artifactId>: ${sourceLabel}`, { code: "maven-pom-missing-artifactId" });
85
+ }
86
+ if (artifactId.includes("${")) {
87
+ throw new MavenPlanError(`POM artifactId is not statically provable: ${sourceLabel}`, { code: "maven-pom-unresolvable-artifactId" });
88
+ }
89
+ // Prefer project-level groupId; fall back to parent groupId when present.
90
+ let groupId = projectDirectChildText(cleaned, "groupId");
91
+ if (!groupId || groupId.includes("${")) {
92
+ const parentBody = parentBlockBody(cleaned);
93
+ if (parentBody) {
94
+ const parentGroup = firstTagText(parentBody, "groupId");
95
+ if (parentGroup && !parentGroup.includes("${")) {
96
+ groupId = parentGroup;
97
+ }
98
+ else {
99
+ groupId = undefined;
100
+ }
101
+ }
102
+ else {
103
+ groupId = undefined;
104
+ }
105
+ }
106
+ const packaging = projectDirectChildText(cleaned, "packaging");
107
+ const modules = collectModuleTexts(cleaned);
108
+ const parentRel = parentRelativePath(cleaned);
109
+ return {
110
+ artifactId,
111
+ groupId,
112
+ packaging: packaging && !packaging.includes("${") ? packaging : undefined,
113
+ modules,
114
+ parentRelativePath: parentRel,
115
+ };
116
+ }
117
+ export function readStaticPomFile(absolutePomPath) {
118
+ let xml;
119
+ try {
120
+ xml = readFileSync(absolutePomPath, "utf8");
121
+ }
122
+ catch (error) {
123
+ throw new MavenPlanError(`Unable to read POM: ${absolutePomPath}`, {
124
+ code: "maven-pom-read-error",
125
+ cause: error,
126
+ });
127
+ }
128
+ return parseStaticPomXml(xml, path.basename(absolutePomPath));
129
+ }
130
+ export function repoRelativePosix(repoRoot, absolutePath) {
131
+ const rel = path.relative(repoRoot, absolutePath);
132
+ if (!rel || rel.startsWith("..") || path.isAbsolute(rel)) {
133
+ throw new MavenPlanError(`Path escapes repository root: ${absolutePath}`, { code: "maven-path-outside-repo" });
134
+ }
135
+ return toPosix(rel);
136
+ }
@@ -0,0 +1,153 @@
1
+ import path from "node:path";
2
+ import { MavenPlanError, } from "./types.js";
3
+ function toPosix(value) {
4
+ return value.replace(/\\/g, "/").replace(/^\.\//, "");
5
+ }
6
+ function stripGlobSuffix(pattern) {
7
+ let p = toPosix(pattern.trim());
8
+ // Normalize common task globs to directory prefixes.
9
+ p = p.replace(/\/\*\*$/, "");
10
+ p = p.replace(/\/\*$/, "");
11
+ p = p.replace(/\*\*$/, "");
12
+ p = p.replace(/\*$/, "");
13
+ p = p.replace(/\/+$/, "");
14
+ return p;
15
+ }
16
+ function isPathInside(parent, child) {
17
+ const p = toPosix(parent);
18
+ const c = toPosix(child);
19
+ if (p === "")
20
+ return true;
21
+ if (c === p)
22
+ return true;
23
+ return c.startsWith(`${p}/`);
24
+ }
25
+ /**
26
+ * A module is proven when at least one scope path is strictly inside that
27
+ * module directory tree (or equals the module dir). Sibling name similarity is
28
+ * never evidence. Broad "**" alone does not prove any module.
29
+ */
30
+ function scopePathProvesModule(scopePath, moduleDir) {
31
+ const scope = stripGlobSuffix(scopePath);
32
+ if (scope === "" || scope === "**" || scope === ".") {
33
+ return false;
34
+ }
35
+ const mod = toPosix(moduleDir);
36
+ if (mod === "") {
37
+ // Root module: only proven by explicit root-ish paths, not by "**".
38
+ return scope === "" || scope === "." || scope === "pom.xml";
39
+ }
40
+ // Scope prefix falls inside module, or module falls inside a write/source path
41
+ // that is more specific than the module parent.
42
+ if (isPathInside(mod, scope))
43
+ return true;
44
+ // writeSet file under module
45
+ if (isPathInside(mod, scope))
46
+ return true;
47
+ return false;
48
+ }
49
+ function collectScopePaths(scope) {
50
+ const values = [
51
+ ...(scope.allowedPaths ?? []),
52
+ ...(scope.writeSet ?? []),
53
+ ...(scope.sourcePaths ?? []),
54
+ ];
55
+ return values.map(toPosix).filter((value) => value.length > 0);
56
+ }
57
+ function nodeDir(node) {
58
+ return toPosix(node.dir);
59
+ }
60
+ function targetForNode(graph, node) {
61
+ const reactorRoot = graph.reactorRootByPomPath[node.pomPath];
62
+ if (reactorRoot && reactorRoot !== node.pomPath) {
63
+ const reactor = graph.reactors.find((r) => r.rootPomPath === reactorRoot);
64
+ // Prefer reactor-root-relative module path for -pl (nested aggregators).
65
+ const coordinate = reactor?.modulePathByPomPath?.[node.pomPath] ??
66
+ reactor?.artifactIdByPomPath[node.pomPath] ??
67
+ node.artifactId;
68
+ return {
69
+ pomPath: node.pomPath,
70
+ artifactId: node.artifactId,
71
+ reactorRootPomPath: reactorRoot,
72
+ moduleCoordinate: coordinate,
73
+ };
74
+ }
75
+ // Root of a reactor selected itself, or independent POM.
76
+ return {
77
+ pomPath: node.pomPath,
78
+ artifactId: node.artifactId,
79
+ reactorRootPomPath: reactorRoot === node.pomPath ? reactorRoot : undefined,
80
+ };
81
+ }
82
+ /**
83
+ * Resolve uniquely proven Maven targets from task scope.
84
+ * Never uses depth, lexical order, or first-match as selection heuristics.
85
+ */
86
+ export function resolveMavenTargets(input) {
87
+ const scopePaths = collectScopePaths(input.scope);
88
+ if (scopePaths.length === 0) {
89
+ throw new MavenPlanError("Maven verification requires task scope paths to prove a unique target; no allowedPaths/writeSet/sourcePaths were provided.", { code: "maven-scope-empty" });
90
+ }
91
+ const proven = [];
92
+ for (const node of input.graph.nodes) {
93
+ const dir = nodeDir(node);
94
+ const provenBy = scopePaths.some((scopePath) => scopePathProvesModule(scopePath, dir));
95
+ if (provenBy) {
96
+ proven.push(node);
97
+ }
98
+ }
99
+ if (proven.length === 0) {
100
+ const candidates = input.graph.nodes.map((n) => n.pomPath).sort();
101
+ throw new MavenPlanError(`Unable to uniquely prove a Maven target from task scope. Candidates: ${candidates.join(", ") || "(none)"}`, { code: "maven-scope-no-target", candidates });
102
+ }
103
+ // If multiple modules are proven, keep them only when each is independently
104
+ // proven by scope — never drop by sorting.
105
+ // Prefer more specific modules over their reactor ancestors when both match
106
+ // because a child path also matches the parent prefix.
107
+ const provenDirs = new Set(proven.map((n) => nodeDir(n)));
108
+ const filtered = proven.filter((node) => {
109
+ const dir = nodeDir(node);
110
+ // Drop ancestor modules when a descendant module is also proven.
111
+ for (const other of proven) {
112
+ if (other.pomPath === node.pomPath)
113
+ continue;
114
+ const otherDir = nodeDir(other);
115
+ if (otherDir !== dir && isPathInside(dir, otherDir)) {
116
+ return false;
117
+ }
118
+ }
119
+ return provenDirs.has(dir);
120
+ });
121
+ if (filtered.length === 0) {
122
+ const candidates = proven.map((n) => n.pomPath).sort();
123
+ throw new MavenPlanError(`Ambiguous Maven target after filtering ancestors. Candidates: ${candidates.join(", ")}`, { code: "maven-scope-ambiguous", candidates });
124
+ }
125
+ // Wide "**" only scope that still matched nothing specific is already handled.
126
+ // If still multiple independent modules and the only scope paths are broad
127
+ // globs that equally cover all of them without nested proof, fail.
128
+ const specificProof = scopePaths.some((scopePath) => {
129
+ const stripped = stripGlobSuffix(scopePath);
130
+ return stripped !== "" && stripped !== "**" && stripped !== ".";
131
+ });
132
+ if (!specificProof && filtered.length > 1) {
133
+ const candidates = filtered.map((n) => n.pomPath).sort();
134
+ throw new MavenPlanError(`Ambiguous Maven task scope matches multiple modules without unique proof. Candidates: ${candidates.join(", ")}`, { code: "maven-scope-ambiguous", candidates });
135
+ }
136
+ // Deduplicate by pom path without sorting-based selection.
137
+ const byPom = new Map();
138
+ for (const node of filtered) {
139
+ byPom.set(node.pomPath, targetForNode(input.graph, node));
140
+ }
141
+ const targets = [...byPom.values()];
142
+ if (targets.length === 0) {
143
+ throw new MavenPlanError("No Maven targets remained after scope resolution.", {
144
+ code: "maven-scope-no-target",
145
+ });
146
+ }
147
+ return targets;
148
+ }
149
+ export function moduleDirFromPomPath(pomPath) {
150
+ const posix = toPosix(pomPath);
151
+ const dir = path.posix.dirname(posix);
152
+ return dir === "." ? "" : dir;
153
+ }
@@ -0,0 +1,130 @@
1
+ import { createHash } from "node:crypto";
2
+ import { isCachedGraphStillFresh } from "./cache.js";
3
+ import { MAVEN_CACHE_SCHEMA_VERSION, MAVEN_PLANNER_VERSION, MavenPlanStaleError, } from "./types.js";
4
+ import { buildMavenWorkspaceGraph } from "./workspace-graph.js";
5
+ function digestCommand(command) {
6
+ return createHash("sha256")
7
+ .update(JSON.stringify({ label: command.label, args: command.args }))
8
+ .digest("hex");
9
+ }
10
+ function digestShellCommand(command) {
11
+ return createHash("sha256").update(command).digest("hex");
12
+ }
13
+ function commandsMatchPlan(plan, commands) {
14
+ if (plan.plannedCommands && plan.plannedCommands.length > 0) {
15
+ if (commands.length !== plan.plannedCommands.length)
16
+ return false;
17
+ for (let i = 0; i < commands.length; i += 1) {
18
+ const planned = plan.plannedCommands[i];
19
+ const actual = commands[i];
20
+ if (planned.label !== actual.label)
21
+ return false;
22
+ if (planned.args.length !== actual.args.length)
23
+ return false;
24
+ for (let j = 0; j < planned.args.length; j += 1) {
25
+ if (planned.args[j] !== actual.args[j])
26
+ return false;
27
+ }
28
+ }
29
+ return true;
30
+ }
31
+ if (plan.commandDigests && plan.commandDigests.length > 0) {
32
+ if (commands.length !== plan.commandDigests.length)
33
+ return false;
34
+ for (let i = 0; i < commands.length; i += 1) {
35
+ if (digestCommand(commands[i]) !== plan.commandDigests[i]) {
36
+ return false;
37
+ }
38
+ }
39
+ return true;
40
+ }
41
+ return true;
42
+ }
43
+ function shellCommandsMatchPlan(plan, shellCommands) {
44
+ if (plan.shellCommandDigests && plan.shellCommandDigests.length > 0) {
45
+ if (shellCommands.length !== plan.shellCommandDigests.length)
46
+ return false;
47
+ for (let i = 0; i < shellCommands.length; i += 1) {
48
+ if (digestShellCommand(shellCommands[i]) !== plan.shellCommandDigests[i]) {
49
+ return false;
50
+ }
51
+ }
52
+ return true;
53
+ }
54
+ // No frozen shell digests — cannot prove drift; treat as matching.
55
+ return true;
56
+ }
57
+ function assertWorkspaceFresh(repoRoot, plan) {
58
+ // Prefer lightweight stat-only check against the frozen manifest.
59
+ if (plan.workspaceManifest) {
60
+ const probe = {
61
+ schemaVersion: plan.schemaVersion,
62
+ plannerVersion: plan.plannerVersion,
63
+ fingerprint: plan.fingerprint,
64
+ pomPaths: [],
65
+ wrapperPaths: [],
66
+ nodes: [],
67
+ reactors: [],
68
+ reactorRootByPomPath: {},
69
+ manifest: plan.workspaceManifest,
70
+ };
71
+ if (isCachedGraphStillFresh(repoRoot, probe)) {
72
+ return;
73
+ }
74
+ // Metadata changed: confirm via content fingerprint (targeted rebuild).
75
+ const graph = buildMavenWorkspaceGraph(repoRoot);
76
+ if (graph.fingerprint !== plan.fingerprint) {
77
+ throw new MavenPlanStaleError("verification-plan-stale: Maven POM/wrapper fingerprint drifted since DAG generation; regenerate/validate the DAG before running Maven");
78
+ }
79
+ // Directory metadata also changes for normal implementation work (for
80
+ // example, adding a Java source directory). The full static rebuild above
81
+ // proves the Maven inputs are unchanged, so the frozen plan remains valid.
82
+ return;
83
+ }
84
+ // Legacy plans without manifest: full static fingerprint re-check.
85
+ const graph = buildMavenWorkspaceGraph(repoRoot);
86
+ if (graph.fingerprint !== plan.fingerprint) {
87
+ throw new MavenPlanStaleError("verification-plan-stale: Maven POM/wrapper fingerprint drifted since DAG generation; regenerate/validate the DAG before running Maven");
88
+ }
89
+ }
90
+ /**
91
+ * Re-check frozen plan against workspace + optional execution command sequence.
92
+ * Lightweight path only stats paths recorded in the frozen workspaceManifest.
93
+ * Throws MavenPlanStaleError (code verification-plan-stale) on drift.
94
+ */
95
+ export function assertMavenPlanFresh(repoRoot, plan, commands) {
96
+ if (plan.schemaVersion !== MAVEN_CACHE_SCHEMA_VERSION) {
97
+ throw new MavenPlanStaleError(`verification-plan-stale: plan schemaVersion ${plan.schemaVersion} != ${MAVEN_CACHE_SCHEMA_VERSION}; regenerate the DAG`);
98
+ }
99
+ if (plan.plannerVersion !== MAVEN_PLANNER_VERSION) {
100
+ throw new MavenPlanStaleError(`verification-plan-stale: plan plannerVersion ${plan.plannerVersion} != ${MAVEN_PLANNER_VERSION}; regenerate the DAG`);
101
+ }
102
+ if (commands) {
103
+ if (Array.isArray(commands)) {
104
+ const normalized = commands.map((command) => ({
105
+ label: command.label,
106
+ args: command.args,
107
+ }));
108
+ if (!commandsMatchPlan(plan, normalized)) {
109
+ throw new MavenPlanStaleError("verification-plan-stale: planned Maven command sequence/digests drifted since DAG generation; regenerate/validate the DAG before running Maven");
110
+ }
111
+ }
112
+ else {
113
+ if (commands.verifyCommands) {
114
+ const normalized = commands.verifyCommands.map((command) => ({
115
+ label: command.label,
116
+ args: command.args,
117
+ }));
118
+ if (!commandsMatchPlan(plan, normalized)) {
119
+ throw new MavenPlanStaleError("verification-plan-stale: planned Maven command sequence/digests drifted since DAG generation; regenerate/validate the DAG before running Maven");
120
+ }
121
+ }
122
+ if (commands.shellCommands) {
123
+ if (!shellCommandsMatchPlan(plan, commands.shellCommands)) {
124
+ throw new MavenPlanStaleError("verification-plan-stale: shell.commands sequence drifted from frozen Maven verification plan; regenerate/validate the DAG before running Maven");
125
+ }
126
+ }
127
+ }
128
+ }
129
+ assertWorkspaceFresh(repoRoot, plan);
130
+ }
@@ -0,0 +1,23 @@
1
+ /** Bump when planner decision rules change (invalidates cache). */
2
+ export const MAVEN_PLANNER_VERSION = "1.1.1";
3
+ /** Bump when on-disk cache JSON shape changes. */
4
+ export const MAVEN_CACHE_SCHEMA_VERSION = 2;
5
+ export class MavenPlanError extends Error {
6
+ code;
7
+ candidates;
8
+ constructor(message, options) {
9
+ super(message, options?.cause ? { cause: options.cause } : undefined);
10
+ this.name = "MavenPlanError";
11
+ this.code = options?.code ?? "maven-plan-error";
12
+ this.candidates = options?.candidates;
13
+ }
14
+ }
15
+ export class MavenPlanStaleError extends MavenPlanError {
16
+ constructor(message, options) {
17
+ super(message, {
18
+ code: "verification-plan-stale",
19
+ cause: options?.cause,
20
+ });
21
+ this.name = "MavenPlanStaleError";
22
+ }
23
+ }