@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,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
+ }
@@ -0,0 +1,421 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { MAVEN_CACHE_SCHEMA_VERSION, MAVEN_PLANNER_VERSION, MavenPlanError, } from "./types.js";
5
+ const MAVEN_LAUNCHERS = new Set([
6
+ "mvn",
7
+ "mvnw",
8
+ "./mvnw",
9
+ ".\\mvnw",
10
+ ".\\mvnw.cmd",
11
+ "mvnw.cmd",
12
+ "mvnw.bat",
13
+ "./mvnw.cmd",
14
+ "./mvnw.bat",
15
+ ]);
16
+ function toPosix(value) {
17
+ return value.replace(/\\/g, "/");
18
+ }
19
+ export function isMavenLauncherToken(token) {
20
+ const normalized = token.replace(/\\/g, "/");
21
+ if (MAVEN_LAUNCHERS.has(token) || MAVEN_LAUNCHERS.has(normalized)) {
22
+ return true;
23
+ }
24
+ const base = path.posix.basename(normalized).toLowerCase();
25
+ return base === "mvn" || base === "mvnw" || base === "mvnw.cmd" || base === "mvnw.bat";
26
+ }
27
+ const ENV_ASSIGNMENT_RE = /^[A-Za-z_][A-Za-z0-9_]*=.*$/;
28
+ /** Detect shell metacharacters that make silent rewrite unsafe. */
29
+ export function assertSafeMavenShellPayload(script) {
30
+ // Strip quoted segments so quoted pipes/redirects do not false-positive,
31
+ // but unquoted operators remain visible.
32
+ const withoutQuotes = script
33
+ .replace(/'[^']*'/g, " ")
34
+ .replace(/"[^"]*"/g, " ");
35
+ if (/[|;&><`]/.test(withoutQuotes) || /\n/.test(script)) {
36
+ throw new MavenPlanError(`Unsafe compound shell payload around Maven (pipeline/redirect/control): ${script}`, { code: "maven-unsafe-shell-payload" });
37
+ }
38
+ if (/\$\(|\$\{/.test(withoutQuotes)) {
39
+ throw new MavenPlanError(`Unsupported shell expansion around Maven: ${script}`, { code: "maven-unsafe-shell-payload" });
40
+ }
41
+ }
42
+ function splitEnvPrefix(tokens) {
43
+ const envPrefix = [];
44
+ let i = 0;
45
+ while (i < tokens.length && ENV_ASSIGNMENT_RE.test(tokens[i])) {
46
+ envPrefix.push(tokens[i]);
47
+ i += 1;
48
+ }
49
+ return { envPrefix, remaining: tokens.slice(i) };
50
+ }
51
+ function tokenizeShellLike(command) {
52
+ const tokens = [];
53
+ let current = "";
54
+ let quote = null;
55
+ for (let i = 0; i < command.length; i += 1) {
56
+ const ch = command[i];
57
+ if (quote) {
58
+ if (ch === quote) {
59
+ quote = null;
60
+ }
61
+ else {
62
+ current += ch;
63
+ }
64
+ continue;
65
+ }
66
+ if (ch === "'" || ch === '"') {
67
+ quote = ch;
68
+ continue;
69
+ }
70
+ if (/\s/.test(ch)) {
71
+ if (current) {
72
+ tokens.push(current);
73
+ current = "";
74
+ }
75
+ continue;
76
+ }
77
+ current += ch;
78
+ }
79
+ if (current)
80
+ tokens.push(current);
81
+ return tokens;
82
+ }
83
+ function joinShellTokens(tokens) {
84
+ return tokens
85
+ .map((token) => {
86
+ if (token.length === 0)
87
+ return "''";
88
+ if (/^[A-Za-z0-9_./:=,@%+-]+$/.test(token))
89
+ return token;
90
+ return `'${token.replace(/'/g, `'\\''`)}'`;
91
+ })
92
+ .join(" ");
93
+ }
94
+ /**
95
+ * Detect Maven invocations in VerifyCommand args.
96
+ * Supports direct `mvn ...` and `bash -lc 'mvn ...'`.
97
+ */
98
+ export function parseMavenInvocation(command) {
99
+ const args = command.args;
100
+ if (args.length === 0)
101
+ return null;
102
+ // bash -lc "mvn ..."
103
+ if (args.length >= 3 &&
104
+ /^(bash|sh|zsh)$/.test(args[0] ?? "") &&
105
+ (args[1] === "-lc" || args[1] === "-c")) {
106
+ const script = args[2] ?? "";
107
+ assertSafeMavenShellPayload(script);
108
+ const tokens = tokenizeShellLike(script);
109
+ const launcherIndex = tokens.findIndex(isMavenLauncherToken);
110
+ if (launcherIndex < 0)
111
+ return null;
112
+ const beforeLauncher = tokens.slice(0, launcherIndex);
113
+ const { envPrefix, remaining } = splitEnvPrefix(beforeLauncher);
114
+ if (remaining.length > 0) {
115
+ throw new MavenPlanError(`Unsupported shell prefix before Maven launcher: ${remaining.join(" ")}`, { code: "maven-unsafe-shell-payload", candidates: remaining });
116
+ }
117
+ return {
118
+ prefix: args.slice(0, 2),
119
+ envPrefix,
120
+ embeddedShell: true,
121
+ embedArgIndex: 2,
122
+ launcher: tokens[launcherIndex],
123
+ mavenArgs: tokens.slice(launcherIndex + 1),
124
+ kind: "bash-lc",
125
+ };
126
+ }
127
+ const launcherIndex = args.findIndex(isMavenLauncherToken);
128
+ if (launcherIndex < 0)
129
+ return null;
130
+ const beforeLauncher = args.slice(0, launcherIndex);
131
+ const { envPrefix, remaining } = splitEnvPrefix(beforeLauncher);
132
+ if (remaining.length > 0) {
133
+ // Non-env prefix tokens on direct argv are preserved as outer prefix
134
+ // (e.g. env wrappers). Only pure env assignments become envPrefix.
135
+ return {
136
+ prefix: beforeLauncher,
137
+ envPrefix: [],
138
+ embeddedShell: false,
139
+ launcher: args[launcherIndex],
140
+ mavenArgs: args.slice(launcherIndex + 1),
141
+ kind: "direct",
142
+ };
143
+ }
144
+ return {
145
+ prefix: [],
146
+ envPrefix,
147
+ embeddedShell: false,
148
+ launcher: args[launcherIndex],
149
+ mavenArgs: args.slice(launcherIndex + 1),
150
+ kind: "direct",
151
+ };
152
+ }
153
+ export function commandContainsMaven(command) {
154
+ return parseMavenInvocation(command) !== null;
155
+ }
156
+ function extractMavenFlags(mavenArgs) {
157
+ const rest = [];
158
+ let explicitFile;
159
+ let explicitPl;
160
+ let explicitAm = false;
161
+ for (let i = 0; i < mavenArgs.length; i += 1) {
162
+ const arg = mavenArgs[i];
163
+ if (arg === "-f" || arg === "--file") {
164
+ const value = mavenArgs[i + 1];
165
+ if (!value) {
166
+ throw new MavenPlanError(`Maven ${arg} is missing a path argument`, {
167
+ code: "maven-flag-missing-value",
168
+ });
169
+ }
170
+ explicitFile = toPosix(value);
171
+ i += 1;
172
+ continue;
173
+ }
174
+ if (arg.startsWith("-f=") || arg.startsWith("--file=")) {
175
+ explicitFile = toPosix(arg.slice(arg.indexOf("=") + 1));
176
+ continue;
177
+ }
178
+ if (arg === "-pl" || arg === "--projects") {
179
+ const value = mavenArgs[i + 1];
180
+ if (!value) {
181
+ throw new MavenPlanError(`Maven ${arg} is missing a value`, {
182
+ code: "maven-flag-missing-value",
183
+ });
184
+ }
185
+ explicitPl = value;
186
+ i += 1;
187
+ continue;
188
+ }
189
+ if (arg.startsWith("-pl=") || arg.startsWith("--projects=")) {
190
+ explicitPl = arg.slice(arg.indexOf("=") + 1);
191
+ continue;
192
+ }
193
+ if (arg === "-am" || arg === "--also-make") {
194
+ explicitAm = true;
195
+ continue;
196
+ }
197
+ rest.push(arg);
198
+ }
199
+ return { explicitFile, explicitPl, explicitAm, rest };
200
+ }
201
+ /** Return explicit -f/--file value from maven args, if present. */
202
+ export function extractExplicitMavenFile(mavenArgs) {
203
+ return extractMavenFlags(mavenArgs).explicitFile;
204
+ }
205
+ function assertInRepoFile(repoRoot, relPath, baseCwd) {
206
+ const raw = toPosix(relPath);
207
+ const absolute = path.isAbsolute(raw)
208
+ ? path.normalize(raw)
209
+ : path.resolve(baseCwd ?? repoRoot, raw);
210
+ const rel = path.relative(repoRoot, absolute);
211
+ if (!rel || rel.startsWith("..") || path.isAbsolute(rel)) {
212
+ throw new MavenPlanError(`Maven -f path escapes repository root: ${relPath}`, { code: "maven-file-outside-repo", candidates: [relPath] });
213
+ }
214
+ if (!existsSync(absolute)) {
215
+ throw new MavenPlanError(`Maven -f path does not exist: ${toPosix(rel)}`, {
216
+ code: "maven-file-missing",
217
+ candidates: [toPosix(rel)],
218
+ });
219
+ }
220
+ return toPosix(rel);
221
+ }
222
+ function groupTargetsByBuild(targets) {
223
+ // reactorRoot -> modules; independent -> each own entry
224
+ const reactorGroups = new Map();
225
+ const independents = [];
226
+ for (const target of targets) {
227
+ if (target.reactorRootPomPath && target.moduleCoordinate) {
228
+ const list = reactorGroups.get(target.reactorRootPomPath) ?? [];
229
+ list.push(target.moduleCoordinate);
230
+ reactorGroups.set(target.reactorRootPomPath, list);
231
+ }
232
+ else if (target.reactorRootPomPath && !target.moduleCoordinate) {
233
+ // Selecting the reactor root itself
234
+ if (!reactorGroups.has(target.reactorRootPomPath)) {
235
+ reactorGroups.set(target.reactorRootPomPath, []);
236
+ }
237
+ }
238
+ else {
239
+ independents.push(target.pomPath);
240
+ }
241
+ }
242
+ const builds = [];
243
+ for (const [root, modules] of reactorGroups.entries()) {
244
+ const uniqueModules = [...new Set(modules)];
245
+ if (uniqueModules.length === 0) {
246
+ builds.push({ filePom: root, plModules: [], useAm: false });
247
+ }
248
+ else {
249
+ builds.push({
250
+ filePom: root,
251
+ plModules: uniqueModules,
252
+ useAm: true,
253
+ });
254
+ }
255
+ }
256
+ for (const pom of independents) {
257
+ builds.push({ filePom: pom, plModules: [], useAm: false });
258
+ }
259
+ // Stable output ordered by filePom path for determinism (not used as selection).
260
+ builds.sort((a, b) => a.filePom.localeCompare(b.filePom));
261
+ return builds;
262
+ }
263
+ function buildMavenArgList(input) {
264
+ const args = ["-f", input.filePom];
265
+ if (input.plModules.length > 0) {
266
+ args.push("-pl", input.plModules.join(","));
267
+ }
268
+ if (input.useAm) {
269
+ args.push("-am");
270
+ }
271
+ args.push(...input.rest);
272
+ return args;
273
+ }
274
+ function rebuildCommand(input) {
275
+ if (input.parsed.kind === "bash-lc" && input.parsed.embeddedShell) {
276
+ const script = joinShellTokens([
277
+ ...input.parsed.envPrefix,
278
+ input.parsed.launcher,
279
+ ...input.mavenArgs,
280
+ ]);
281
+ const args = [...input.original.args];
282
+ args[input.parsed.embedArgIndex ?? 2] = script;
283
+ return {
284
+ ...input.original,
285
+ args,
286
+ };
287
+ }
288
+ return {
289
+ ...input.original,
290
+ args: [
291
+ ...input.parsed.prefix,
292
+ ...input.parsed.envPrefix,
293
+ input.parsed.launcher,
294
+ ...input.mavenArgs,
295
+ ],
296
+ };
297
+ }
298
+ function digestCommand(command) {
299
+ return createHash("sha256")
300
+ .update(JSON.stringify({ label: command.label, args: command.args }))
301
+ .digest("hex");
302
+ }
303
+ function digestInputs(input) {
304
+ return createHash("sha256")
305
+ .update(JSON.stringify({
306
+ commands: input.commands.map((c) => ({
307
+ label: c.label,
308
+ args: c.args,
309
+ })),
310
+ targets: input.targets,
311
+ scope: {
312
+ allowedPaths: input.scope.allowedPaths,
313
+ writeSet: input.scope.writeSet ?? [],
314
+ sourcePaths: input.scope.sourcePaths ?? [],
315
+ },
316
+ plannerVersion: MAVEN_PLANNER_VERSION,
317
+ }))
318
+ .digest("hex");
319
+ }
320
+ /**
321
+ * Rewrite Maven VerifyCommands for the resolved targets.
322
+ * Non-Maven commands pass through unchanged.
323
+ */
324
+ export function planMavenCommands(input) {
325
+ const builds = groupTargetsByBuild(input.targets);
326
+ const planned = [];
327
+ for (const command of input.commands) {
328
+ const parsed = parseMavenInvocation(command);
329
+ if (!parsed) {
330
+ planned.push(command);
331
+ continue;
332
+ }
333
+ const flags = extractMavenFlags(parsed.mavenArgs);
334
+ if (flags.explicitFile) {
335
+ const filePom = assertInRepoFile(input.repoRoot, flags.explicitFile, command.cwd ?? input.repoRoot);
336
+ // Explicit -f: respect user target; still forbid conflicting auto -pl when
337
+ // the file is an independent module pom without those modules.
338
+ if (flags.explicitPl || flags.explicitAm) {
339
+ const node = input.graph.nodes.find((n) => n.pomPath === filePom);
340
+ const isReactorRoot = input.graph.reactors.some((r) => r.rootPomPath === filePom);
341
+ if (!isReactorRoot) {
342
+ throw new MavenPlanError(`Explicit -pl/-am conflicts with automatic planning for non-reactor POM -f ${filePom}`, {
343
+ code: "maven-explicit-pl-conflict",
344
+ candidates: [filePom, flags.explicitPl ?? "-am"],
345
+ });
346
+ }
347
+ // When user already provided full -f/-pl/-am, keep rest + their flags.
348
+ const mavenArgs = buildMavenArgList({
349
+ filePom,
350
+ plModules: flags.explicitPl
351
+ ? flags.explicitPl.split(",").map((s) => s.trim()).filter(Boolean)
352
+ : [],
353
+ useAm: flags.explicitAm,
354
+ rest: flags.rest,
355
+ });
356
+ // Avoid double -f from buildMavenArgList — already correct.
357
+ planned.push(rebuildCommand({
358
+ original: command,
359
+ parsed,
360
+ mavenArgs,
361
+ }));
362
+ continue;
363
+ }
364
+ // Explicit -f without -pl: keep that single target.
365
+ const mavenArgs = buildMavenArgList({
366
+ filePom,
367
+ plModules: [],
368
+ useAm: false,
369
+ rest: flags.rest,
370
+ });
371
+ planned.push(rebuildCommand({ original: command, parsed, mavenArgs }));
372
+ continue;
373
+ }
374
+ if (flags.explicitPl || flags.explicitAm) {
375
+ throw new MavenPlanError("Existing -pl/-am without a validated -f conflicts with automatic Maven planning; provide an explicit in-repo -f or remove -pl/-am.", {
376
+ code: "maven-explicit-pl-conflict",
377
+ candidates: [
378
+ flags.explicitPl ?? "",
379
+ flags.explicitAm ? "-am" : "",
380
+ ].filter(Boolean),
381
+ });
382
+ }
383
+ // Auto plan: expand one original command into one command per build group.
384
+ for (const build of builds) {
385
+ const mavenArgs = buildMavenArgList({
386
+ filePom: build.filePom,
387
+ plModules: build.plModules,
388
+ useAm: build.useAm,
389
+ rest: flags.rest,
390
+ });
391
+ planned.push(rebuildCommand({
392
+ original: {
393
+ ...command,
394
+ label: builds.length > 1
395
+ ? `${command.label}:${build.filePom}`
396
+ : command.label,
397
+ },
398
+ parsed,
399
+ mavenArgs,
400
+ }));
401
+ }
402
+ }
403
+ const plan = {
404
+ schemaVersion: MAVEN_CACHE_SCHEMA_VERSION,
405
+ plannerVersion: MAVEN_PLANNER_VERSION,
406
+ fingerprint: input.graph.fingerprint,
407
+ inputDigest: digestInputs({
408
+ commands: input.commands,
409
+ targets: input.targets,
410
+ scope: input.scope,
411
+ }),
412
+ targets: input.targets,
413
+ commandDigests: planned.map(digestCommand),
414
+ plannedCommands: planned.map((command) => ({
415
+ label: command.label,
416
+ args: command.args,
417
+ })),
418
+ workspaceManifest: input.graph.manifest,
419
+ };
420
+ return { commands: planned, plan };
421
+ }