@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
@@ -0,0 +1,198 @@
1
+ import { closeSync, fstatSync, fsyncSync, ftruncateSync, lstatSync, mkdtempSync, openSync, realpathSync, rmdirSync, unlinkSync, writeFileSync, } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import path from "node:path";
4
+ const PROMPT_DIRECTORY_PREFIX = "loop-agent-pi-prompt-";
5
+ const PROMPT_FILE_NAME = "system-prompt.md";
6
+ function identityOf(stats) {
7
+ return { dev: stats.dev, ino: stats.ino };
8
+ }
9
+ function hasIdentity(stats, identity) {
10
+ return stats.dev === identity.dev && stats.ino === identity.ino;
11
+ }
12
+ function normalizedPath(value) {
13
+ const resolved = path.resolve(value);
14
+ return process.platform === "win32" ? resolved.toLowerCase() : resolved;
15
+ }
16
+ function assertOwnedPromptPaths(transport) {
17
+ const tempRoot = normalizedPath(transport.tempRoot);
18
+ const directoryPath = normalizedPath(transport.directoryPath);
19
+ const filePath = normalizedPath(transport.filePath);
20
+ const tempRootStats = lstatSync(transport.tempRoot, { bigint: true });
21
+ if (!tempRootStats.isDirectory() ||
22
+ tempRootStats.isSymbolicLink() ||
23
+ !hasIdentity(tempRootStats, transport.tempRootIdentity)) {
24
+ throw new Error("Pi prompt cleanup refused: temporary root identity changed");
25
+ }
26
+ if (normalizedPath(path.dirname(transport.directoryPath)) !== tempRoot) {
27
+ throw new Error("Pi prompt cleanup refused: temporary directory escaped its recorded root");
28
+ }
29
+ if (!path.basename(directoryPath).startsWith(PROMPT_DIRECTORY_PREFIX)) {
30
+ throw new Error("Pi prompt cleanup refused: unexpected temporary directory name");
31
+ }
32
+ if (normalizedPath(path.dirname(transport.filePath)) !== directoryPath) {
33
+ throw new Error("Pi prompt cleanup refused: prompt file escaped its recorded directory");
34
+ }
35
+ if (path.basename(filePath) !== PROMPT_FILE_NAME) {
36
+ throw new Error("Pi prompt cleanup refused: unexpected prompt file name");
37
+ }
38
+ }
39
+ export function createPiPromptTransport(prompt) {
40
+ const tempRoot = realpathSync.native(tmpdir());
41
+ const tempRootStats = lstatSync(tempRoot, { bigint: true });
42
+ if (!tempRootStats.isDirectory() || tempRootStats.isSymbolicLink()) {
43
+ throw new Error("Pi prompt transport requires a real temporary directory");
44
+ }
45
+ const tempRootIdentity = identityOf(tempRootStats);
46
+ const directoryPath = mkdtempSync(path.join(tempRoot, PROMPT_DIRECTORY_PREFIX));
47
+ const currentTempRootStats = lstatSync(tempRoot, { bigint: true });
48
+ if (!hasIdentity(currentTempRootStats, tempRootIdentity)) {
49
+ throw new Error("Pi prompt transport temporary root changed during creation");
50
+ }
51
+ const directoryStats = lstatSync(directoryPath, { bigint: true });
52
+ if (!directoryStats.isDirectory() || directoryStats.isSymbolicLink()) {
53
+ throw new Error("Pi prompt transport did not create a private directory");
54
+ }
55
+ const filePath = path.join(directoryPath, PROMPT_FILE_NAME);
56
+ let fileDescriptor;
57
+ let fileIdentity;
58
+ try {
59
+ fileDescriptor = openSync(filePath, "wx", 0o600);
60
+ fileIdentity = identityOf(fstatSync(fileDescriptor, { bigint: true }));
61
+ writeFileSync(fileDescriptor, prompt, { encoding: "utf-8" });
62
+ fsyncSync(fileDescriptor);
63
+ return {
64
+ directoryPath,
65
+ filePath,
66
+ fileDescriptor,
67
+ tempRoot,
68
+ tempRootIdentity,
69
+ directoryIdentity: identityOf(directoryStats),
70
+ fileIdentity,
71
+ };
72
+ }
73
+ catch (error) {
74
+ if (fileDescriptor !== undefined && fileIdentity !== undefined) {
75
+ try {
76
+ cleanupPiPromptTransport({
77
+ directoryPath,
78
+ filePath,
79
+ fileDescriptor,
80
+ tempRoot,
81
+ tempRootIdentity,
82
+ directoryIdentity: identityOf(directoryStats),
83
+ fileIdentity,
84
+ });
85
+ }
86
+ catch {
87
+ // Creation failed; the original error remains authoritative.
88
+ }
89
+ }
90
+ else {
91
+ try {
92
+ if (fileDescriptor !== undefined)
93
+ closeSync(fileDescriptor);
94
+ }
95
+ catch {
96
+ // Best effort before rethrowing the creation error.
97
+ }
98
+ try {
99
+ const currentDirectory = lstatSync(directoryPath, { bigint: true });
100
+ if (hasIdentity(currentDirectory, identityOf(directoryStats))) {
101
+ rmdirSync(directoryPath);
102
+ }
103
+ }
104
+ catch {
105
+ // Never recursively remove a transport directory.
106
+ }
107
+ }
108
+ throw error;
109
+ }
110
+ }
111
+ /**
112
+ * Clear and remove only the exact prompt file created by this transport.
113
+ * The directory is removed only when it is still the same empty directory.
114
+ */
115
+ export function cleanupPiPromptTransport(transport) {
116
+ const warnings = [];
117
+ let contentCleared = false;
118
+ let fileRemoved = false;
119
+ let directoryRemoved = false;
120
+ let truncateError;
121
+ try {
122
+ ftruncateSync(transport.fileDescriptor, 0);
123
+ fsyncSync(transport.fileDescriptor);
124
+ contentCleared = true;
125
+ }
126
+ catch (error) {
127
+ truncateError = error;
128
+ }
129
+ finally {
130
+ try {
131
+ closeSync(transport.fileDescriptor);
132
+ }
133
+ catch {
134
+ // A closed descriptor is acceptable; identity checks still guard deletion.
135
+ }
136
+ }
137
+ let pathsOwned = false;
138
+ try {
139
+ assertOwnedPromptPaths(transport);
140
+ const directoryStats = lstatSync(transport.directoryPath, { bigint: true });
141
+ const fileStats = lstatSync(transport.filePath, { bigint: true });
142
+ pathsOwned =
143
+ directoryStats.isDirectory() &&
144
+ !directoryStats.isSymbolicLink() &&
145
+ hasIdentity(directoryStats, transport.directoryIdentity) &&
146
+ fileStats.isFile() &&
147
+ !fileStats.isSymbolicLink() &&
148
+ hasIdentity(fileStats, transport.fileIdentity);
149
+ if (!pathsOwned) {
150
+ warnings.push("Pi prompt cleanup refused deletion because path identity changed");
151
+ }
152
+ }
153
+ catch (error) {
154
+ const code = error.code;
155
+ if (code === "ENOENT") {
156
+ fileRemoved = true;
157
+ }
158
+ else {
159
+ warnings.push(`Pi prompt cleanup could not verify the exact prompt path: ${error instanceof Error ? error.message : String(error)}`);
160
+ }
161
+ }
162
+ if (pathsOwned) {
163
+ try {
164
+ unlinkSync(transport.filePath);
165
+ fileRemoved = true;
166
+ }
167
+ catch (error) {
168
+ warnings.push(`Pi prompt cleanup could not unlink the exact prompt file: ${error instanceof Error ? error.message : String(error)}`);
169
+ }
170
+ }
171
+ if (!contentCleared && !fileRemoved) {
172
+ throw new Error(`Pi prompt cleanup could not prove prompt removal: ${truncateError instanceof Error ? truncateError.message : String(truncateError ?? "unknown truncate failure")}`);
173
+ }
174
+ if (fileRemoved) {
175
+ try {
176
+ const directoryStats = lstatSync(transport.directoryPath, { bigint: true });
177
+ if (directoryStats.isDirectory() &&
178
+ !directoryStats.isSymbolicLink() &&
179
+ hasIdentity(directoryStats, transport.directoryIdentity)) {
180
+ rmdirSync(transport.directoryPath);
181
+ directoryRemoved = true;
182
+ }
183
+ else {
184
+ warnings.push("Pi prompt cleanup refused directory removal because identity changed");
185
+ }
186
+ }
187
+ catch (error) {
188
+ const code = error.code;
189
+ if (code === "ENOENT") {
190
+ directoryRemoved = true;
191
+ }
192
+ else {
193
+ warnings.push(`Pi prompt cleanup left an empty or non-empty transport directory: ${error instanceof Error ? error.message : String(error)}`);
194
+ }
195
+ }
196
+ }
197
+ return { contentCleared, fileRemoved, directoryRemoved, warnings };
198
+ }
@@ -296,7 +296,7 @@ export async function executeSingleSdkAttempt(options) {
296
296
  '--model', modelConfig.model ?? '(default)',
297
297
  ...(modelConfig.thinking ? ['--thinking', modelConfig.thinking] : []),
298
298
  '--tools', options.toolNames.join(','),
299
- '--append-system-prompt', options.prompt,
299
+ '--append-system-prompt', '<in-memory-system-prompt>',
300
300
  ...options.attachedFiles.map((file) => `@${file}`),
301
301
  options.userMessage,
302
302
  ];
@@ -0,0 +1,33 @@
1
+ import { spawnSync } from "node:child_process";
2
+ /** Terminate a command and all descendants that it started. */
3
+ export function terminateProcessTree(child, signal, dependencies = {}) {
4
+ if (child.exitCode !== null && child.exitCode !== undefined)
5
+ return true;
6
+ const pid = child.pid;
7
+ if (!pid)
8
+ return child.kill(signal);
9
+ const platform = dependencies.platform ?? process.platform;
10
+ if (platform === "win32") {
11
+ const runTaskkill = dependencies.runTaskkill ?? ((targetPid) => {
12
+ const result = spawnSync("taskkill.exe", ["/pid", String(targetPid), "/t", "/f"], { stdio: "ignore", windowsHide: true });
13
+ return !result.error && result.status === 0;
14
+ });
15
+ return runTaskkill(pid) || child.kill(signal);
16
+ }
17
+ try {
18
+ const killProcessGroup = dependencies.killProcessGroup ?? ((targetPid, targetSignal) => {
19
+ process.kill(-targetPid, targetSignal);
20
+ });
21
+ killProcessGroup(pid, signal);
22
+ return true;
23
+ }
24
+ catch {
25
+ return child.kill(signal);
26
+ }
27
+ }
28
+ export function processTreeSpawnOptions() {
29
+ return {
30
+ detached: process.platform !== "win32",
31
+ windowsHide: true,
32
+ };
33
+ }