@tea-agent/loop-agent 0.33.6 → 0.33.7-beta.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 (69) hide show
  1. package/CHANGELOG.md +23 -23
  2. package/dist/application/task-lifecycle/advance.js +4 -254
  3. package/dist/application/task-lifecycle/gates.js +0 -50
  4. package/dist/application/task-lifecycle/observe.js +2 -11
  5. package/dist/commands/init-upgrade.js +1 -32
  6. package/dist/commands/init.js +3 -94
  7. package/dist/executors/shell-executor.js +91 -4
  8. package/dist/executors/shell-write-guard.js +8 -26
  9. package/dist/shared/operator/capabilities.js +42 -72
  10. package/dist/task/source-prepare/index.js +0 -2
  11. package/dist/task/source-prepare/parse-intent.js +10 -58
  12. package/dist/task/source-prepare/prepare.js +16 -180
  13. package/dist/task/source-prepare/reference-integrity.js +2 -18
  14. package/dist/worker/console/app-data.js +0 -2
  15. package/dist/worker/console/chat/chat-event-store.js +25 -190
  16. package/dist/worker/console/chat/instruction-skills.js +217 -0
  17. package/dist/worker/console/chat/pi-console-config.js +32 -250
  18. package/dist/worker/console/chat/pi-runtime.js +71 -625
  19. package/dist/worker/console/chat/resource-loader.js +4 -5
  20. package/dist/worker/console/chat/routes.js +146 -324
  21. package/dist/worker/console/chat/runtime-context.js +12 -48
  22. package/dist/worker/console/chat/runtime-selection.js +0 -59
  23. package/dist/worker/console/chat/shortcuts.js +0 -1
  24. package/dist/worker/console/chat/tool-adapter.js +3 -9
  25. package/dist/worker/console/chat/tools.js +1 -5
  26. package/dist/worker/console/operator-actions.js +68 -559
  27. package/dist/worker/console/server.js +15 -8
  28. package/dist/worker/console/static/assets/index-CnUXAqxG.css +1 -0
  29. package/dist/worker/console/static/assets/index-CteJFFL2.js +29 -0
  30. package/dist/worker/console/static/index.html +2 -2
  31. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +8 -45
  32. package/dist/worker/console/static-src/operator-chat/refs.js +0 -9
  33. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +0 -16
  34. package/dist/worker/console/static-src/operator-chat/useChatStream.js +184 -210
  35. package/dist/worker/console/static-src/operator-chat/useChatThread.js +5 -49
  36. package/dist/worker/console/static-src/operator-chat/useComposer.js +0 -17
  37. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +74 -225
  38. package/dist/worker/delivery/final-verification.js +5 -13
  39. package/dist/worker/delivery/package.js +19 -31
  40. package/dist/worker/delivery/verification-bundle.js +4 -6
  41. package/dist/worker/observe/static/operator-chrome.css +2 -5
  42. package/dist/worker/observe/static/operator-chrome.js +1 -6
  43. package/dist/worker/observe/static/styles.css +9 -39
  44. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +462 -33
  45. package/dist/workflows/dag/backend-test-case-manifest.js +4 -0
  46. package/dist/workflows/dag/backend-test-markdown-workflow.js +25 -1
  47. package/dist/workflows/dag/backend-test-module-stem.js +5 -0
  48. package/dist/workflows/dag/backend-test-pytest-collection.js +345 -24
  49. package/dist/workflows/dag/backend-test-scenario-param.js +269 -82
  50. package/dist/workflows/dag/backend-test-writer-completeness.js +47 -16
  51. package/dist/workflows/dag/dynamic-runtime/map.js +24 -8
  52. package/dist/workflows/dag/frontend-worktree-diff.js +27 -12
  53. package/dist/workflows/dag/init-hybrid.js +46 -34
  54. package/dist/workflows/dag/types.js +7 -0
  55. package/dist/workflows/dag/workspace-checkpoint.js +27 -8
  56. package/docs/templates/backend-test-dag.json +32 -29
  57. package/harness.json +1 -1
  58. package/package.json +1 -1
  59. package/skills/loop-agent/references/command-reference.md +1 -3
  60. package/skills/loop-agent/references/source-and-plan-practice.md +0 -13
  61. package/skills/loop-agent/references/task-workflow.md +0 -4
  62. package/dist/shared/resilient-git.js +0 -133
  63. package/dist/task/source-prepare/artifact-meta.js +0 -137
  64. package/dist/task/source-prepare/semantic-intake.js +0 -404
  65. package/dist/worker/console/dag-execution-receipt.js +0 -380
  66. package/dist/worker/console/static/assets/index-BUOLppPr.js +0 -28
  67. package/dist/worker/console/static/assets/index-C1KzazY5.css +0 -1
  68. package/dist/worker/console/static-src/operator-chat/runtime-snapshot-store.js +0 -257
  69. package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +0 -196
@@ -1,6 +1,8 @@
1
1
  import { createHash, randomBytes } from "node:crypto";
2
2
  import { open, readFile, rename, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
+ import { readJsonIfExists, writeSecureJson } from "../app-data.js";
5
+ import { OPERATOR_CHAT_ALLOWED_INSTRUCTION_SKILLS } from "./instruction-skills.js";
4
6
  const revision = (raw) => createHash("sha256").update(raw).digest("hex");
5
7
  export async function readModelsConfig(agentDir) {
6
8
  let raw = "{}";
@@ -108,219 +110,26 @@ export async function patchModelsConfig(agentDir, input) {
108
110
  await rename(tmp, file);
109
111
  return readModelsConfig(agentDir);
110
112
  }
111
- /** Max bytes of a package.json we are willing to read (read-only, bounded). */
112
- export const MAX_PACKAGE_JSON_BYTES = 64 * 1024;
113
- /** Max directory levels walked upward from an owned resolved resource path. */
114
- export const MAX_PACKAGE_JSON_WALK_DEPTH = 24;
115
- /**
116
- * One npm-style range token: optional comparator/v-prefix followed by an
117
- * exact/x-form version, a wildcard, or a prerelease/build suffix. Accepts
118
- * >=2.0.0, <3.0.0, ^2.1.0, ~1.2.3, ~1, 1.2, 1.x, 1.2.x, 1.2.3-beta.1+build.5, *.
119
- */
120
- const SPEC_RANGE_TOKEN = "(?:>=|<=|[<>=~^*v])?(?:\\d+(?:\\.(?:\\d+|x|X)){0,2}(?:-[0-9A-Za-z.-]+)?(?:\\+[0-9A-Za-z.-]+)?|\\*|x|X)";
121
- /** Whitespace-separated comparator list, e.g. ">=2.0.0 <3.0.0". */
122
- const SPEC_RANGE = new RegExp(`^${SPEC_RANGE_TOKEN}(?:\\s+${SPEC_RANGE_TOKEN})*$`);
123
- /**
124
- * Installed-side grammar: an EXACT semver with optional prerelease and build
125
- * metadata (1.2.3, 1.2.3-beta.1, 1.2.3+build.5, 1.2.3-beta.1+build.5). Ranges
126
- * and partial shapes (*, 1.x, ~1, 1.2) are rejected with a field diagnostic.
127
- */
128
- const INSTALLED_VERSION = /^v?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
129
- /**
130
- * Try to derive the explicit version/range from an npm-style spec — verbatim
131
- * projection, zero resolution and zero install (AC-RF-1). Git/URL shapes
132
- * (containing /, #, :// or git+) never project a configuredVersion.
133
- */
134
- function versionFromSpec(spec) {
135
- // npm:name@1.2.3 / name@^1.2 / @scope/name@1.2.3 — never guess on git/url specs.
136
- const match = spec.match(/^(?:npm:)?(@?[^@/\s]+(?:\/[^@\s]+)?)@(.+)$/);
137
- if (!match)
138
- return undefined;
139
- const version = match[2];
140
- if (/[\/#:]|git\+/.test(version))
141
- return undefined;
142
- return SPEC_RANGE.test(version) ? version : undefined;
113
+ const preferencesFile = (appData) => path.join(appData.chats, ".instruction-skill-preferences.json");
114
+ export async function readSkillPreferences(appData) {
115
+ const value = await readJsonIfExists(preferencesFile(appData));
116
+ return (value?.disabledNames ?? []).filter((name) => OPERATOR_CHAT_ALLOWED_INSTRUCTION_SKILLS.includes(name));
143
117
  }
144
- /**
145
- * Bounded read of a package.json: open + single read capped at
146
- * MAX_PACKAGE_JSON_BYTES+1 so an oversized file is detected without
147
- * buffering the whole file, then JSON.parse with root-object validation.
148
- */
149
- async function readPackageJsonBounded(file) {
150
- let handle;
151
- try {
152
- handle = await open(file, "r");
153
- }
154
- catch (error) {
155
- const code = error.code;
156
- if (code === "ENOENT")
157
- return { ok: false, missing: true, diagnostic: "" };
158
- return {
159
- ok: false,
160
- diagnostic: `PACKAGE_JSON_READ_FAILED: ${file} (${error instanceof Error ? error.message : String(error)})`,
161
- };
162
- }
163
- try {
164
- const buffer = Buffer.alloc(MAX_PACKAGE_JSON_BYTES + 1);
165
- const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
166
- if (bytesRead > MAX_PACKAGE_JSON_BYTES) {
167
- return {
168
- ok: false,
169
- diagnostic: `PACKAGE_JSON_TOO_LARGE: ${file} (exceeds ${MAX_PACKAGE_JSON_BYTES} bytes)`,
170
- };
171
- }
172
- const raw = buffer.subarray(0, bytesRead).toString("utf8");
173
- let parsed;
174
- try {
175
- parsed = JSON.parse(raw);
176
- }
177
- catch (error) {
178
- return {
179
- ok: false,
180
- diagnostic: `PACKAGE_JSON_INVALID: ${file} (${error instanceof Error ? error.message : String(error)})`,
181
- };
182
- }
183
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
184
- return {
185
- ok: false,
186
- diagnostic: `PACKAGE_JSON_INVALID: ${file} (root must be a JSON object)`,
187
- };
188
- }
189
- const record = parsed;
190
- return { ok: true, value: { name: record.name, version: record.version } };
191
- }
192
- finally {
193
- await handle.close().catch(() => { });
194
- }
195
- }
196
- /**
197
- * Owning npm package root for a resolved entry directory: the deepest
198
- * ancestor BELOW the nearest node_modules directory (scoped:
199
- * node_modules/@scope/<pkg>). Returns undefined when the entry is not inside
200
- * any node_modules boundary — readOwnedPackageJson then degrades with
201
- * PACKAGE_JSON_NOT_FOUND instead of walking to an ancestor manifest.
202
- */
203
- function packageBoundaryRoot(startDir) {
204
- let dir = startDir;
205
- for (let depth = 0; depth < MAX_PACKAGE_JSON_WALK_DEPTH; depth += 1) {
206
- if (path.basename(dir) === "node_modules") {
207
- const segments = path
208
- .relative(dir, startDir)
209
- .split(path.sep)
210
- .filter((segment) => segment.length > 0);
211
- if (segments.length === 0)
212
- return dir;
213
- if (segments[0].startsWith("@") && segments.length >= 2) {
214
- return path.join(dir, segments[0], segments[1]);
215
- }
216
- return path.join(dir, segments[0]);
217
- }
218
- const parent = path.dirname(dir);
219
- if (parent === dir)
220
- return undefined;
221
- dir = parent;
222
- }
223
- return undefined;
224
- }
225
- /**
226
- * Walk UP from an owned resolved resource path (bounded, separator-normalized)
227
- * to the NEAREST package.json within the owning npm package boundary and
228
- * project its validated metadata. The walk NEVER crosses the package root
229
- * (node_modules/<pkg> or node_modules/@scope/<pkg>): a missing manifest there
230
- * yields PACKAGE_JSON_NOT_FOUND instead of mis-attributing an ancestor
231
- * workspace manifest (AC-RF-3). Missing name/version fields yield per-field
232
- * PACKAGE_JSON_FIELD_INVALID diagnostics and suppress the whole metadata trio
233
- * (AC-RF-4). Corrupt / oversized / invalid-field reads stop at that nearest
234
- * package.json with a per-package diagnostic. Empty paths never probe the
235
- * filesystem.
236
- */
237
- async function readOwnedPackageJson(entryPath) {
238
- if (!entryPath)
239
- return { ok: false, diagnostics: [] };
240
- const normalized = entryPath.replace(/[\\/]+/g, path.sep);
241
- const startDir = path.dirname(normalized);
242
- const boundaryRoot = packageBoundaryRoot(startDir);
243
- if (boundaryRoot === undefined) {
244
- // No legal node_modules/<pkg> or node_modules/@scope/<pkg> boundary:
245
- // never walk to (or accept) an ancestor manifest (AC-B1/AC-B2).
246
- return {
247
- ok: false,
248
- diagnostics: [
249
- `PACKAGE_JSON_NOT_FOUND: ${startDir} (resolved resource is not inside a legal node_modules package boundary)`,
250
- ],
251
- };
252
- }
253
- let dir = startDir;
254
- for (let depth = 0; depth < MAX_PACKAGE_JSON_WALK_DEPTH; depth += 1) {
255
- const file = path.join(dir, "package.json");
256
- const read = await readPackageJsonBounded(file);
257
- if (read.ok) {
258
- const { name, version } = read.value;
259
- const meta = { installedPath: dir };
260
- const fieldDiagnostics = [];
261
- if (name === undefined) {
262
- fieldDiagnostics.push(`PACKAGE_JSON_FIELD_INVALID: ${file} (missing name)`);
263
- }
264
- else if (typeof name !== "string" ||
265
- name.trim().length === 0 ||
266
- name.length > 214) {
267
- fieldDiagnostics.push(`PACKAGE_JSON_FIELD_INVALID: ${file} (name must be a non-empty string ≤ 214 chars)`);
268
- }
269
- else {
270
- meta.packageName = name;
271
- }
272
- if (version === undefined) {
273
- fieldDiagnostics.push(`PACKAGE_JSON_FIELD_INVALID: ${file} (missing version)`);
274
- }
275
- else if (typeof version !== "string" || !INSTALLED_VERSION.test(version)) {
276
- // Type + format validation only: the browser projection (80-char limit)
277
- // is the single truncation authority for versions (AC-8 reuse chain).
278
- fieldDiagnostics.push(`PACKAGE_JSON_FIELD_INVALID: ${file} (version must be an exact semver string)`);
279
- }
280
- else {
281
- meta.version = version;
282
- }
283
- if (name === undefined || version === undefined) {
284
- // A manifest missing a required field cannot prove package ownership:
285
- // none of packageName/version/installedPath is projected (AC-RF-4).
286
- return { ok: false, diagnostics: fieldDiagnostics };
287
- }
288
- return {
289
- ok: true,
290
- meta,
291
- ...(fieldDiagnostics.length > 0
292
- ? { diagnostics: fieldDiagnostics }
293
- : {}),
294
- };
295
- }
296
- // Missing file (ENOENT): keep walking up the owned chain, but never past
297
- // the owning package root. Any other read/parse failure means the nearest
298
- // package.json IS the package boundary — degrade only this package and
299
- // stop (AC-5).
300
- if (read.missing !== true) {
301
- return { ok: false, diagnostics: [read.diagnostic] };
302
- }
303
- if (boundaryRoot !== undefined && dir === boundaryRoot) {
304
- return {
305
- ok: false,
306
- diagnostics: [
307
- `PACKAGE_JSON_NOT_FOUND: ${dir} (no package.json within the owning npm package boundary)`,
308
- ],
309
- };
310
- }
311
- const parent = path.dirname(dir);
312
- if (parent === dir)
313
- break;
314
- dir = parent;
315
- }
316
- return {
317
- ok: false,
318
- diagnostics: [
319
- `PACKAGE_JSON_NOT_FOUND: ${startDir} (no package.json on the resolved resource chain)`,
320
- ],
321
- };
118
+ export async function writeSkillPreferences(appData, disabledNames) {
119
+ const allowed = new Set(OPERATOR_CHAT_ALLOWED_INSTRUCTION_SKILLS);
120
+ if (disabledNames.some((name) => !allowed.has(name)))
121
+ throw Object.assign(new Error("preferences contain a non-allowed skill"), {
122
+ code: "INVALID_INPUT",
123
+ status: 400,
124
+ });
125
+ const normalized = [...new Set(disabledNames)].sort();
126
+ await writeSecureJson(preferencesFile(appData), {
127
+ schemaVersion: 1,
128
+ disabledNames: normalized,
129
+ });
130
+ return normalized;
322
131
  }
323
- export async function readPackageInventory(agentDir, repoRoot, resolved) {
132
+ export async function readPackageInventory(agentDir, repoRoot) {
324
133
  const read = async (file) => {
325
134
  try {
326
135
  const parsed = JSON.parse(await readFile(file, "utf8"));
@@ -332,45 +141,18 @@ export async function readPackageInventory(agentDir, repoRoot, resolved) {
332
141
  };
333
142
  const global = await read(path.join(agentDir, "settings.json"));
334
143
  const project = await read(path.join(repoRoot, ".pi", "settings.json"));
335
- const specs = [
336
- ...global.map((spec) => ({ spec, scope: "global" })),
337
- ...project.map((spec) => ({ spec, scope: "project" })),
338
- ];
339
- const entries = [];
340
- for (const { spec, scope } of specs) {
341
- // A resource belongs to this package when its source matches the spec
342
- // exactly or ends with it (e.g. package subpath sources).
343
- const owned = (resolved ?? []).filter((item) => item.source === spec || item.source.endsWith(`/${spec}`));
344
- const diagnostics = owned
345
- .map((item) => item.scope === "temporary" ? `temporary scope: ${item.path}` : "")
346
- .filter(Boolean);
347
- // No owned resources → no filesystem probing at all (AC-1): the optional
348
- // metadata fields stay absent and only the always-projected cwd is added.
349
- const meta = owned.length > 0 && owned[0]?.path
350
- ? await readOwnedPackageJson(owned[0].path)
351
- : { ok: true, meta: undefined, diagnostics: [] };
352
- if (meta.diagnostics)
353
- diagnostics.push(...meta.diagnostics);
354
- entries.push({
144
+ return [
145
+ ...global.map((spec) => ({
355
146
  spec,
356
- scope,
147
+ scope: "global",
357
148
  enabled: true,
358
- cwd: repoRoot,
359
- ...(versionFromSpec(spec)
360
- ? { configuredVersion: versionFromSpec(spec) }
361
- : {}),
362
- ...(meta.ok && meta.meta?.packageName
363
- ? { packageName: meta.meta.packageName }
364
- : {}),
365
- ...(meta.ok && meta.meta?.version ? { version: meta.meta.version } : {}),
366
- ...(meta.ok && meta.meta?.installedPath
367
- ? { installedPath: meta.meta.installedPath }
368
- : {}),
369
- ...(owned[0]?.path ? { path: owned[0].path } : {}),
370
- resolved: owned,
371
- resourceCount: owned.length,
372
- diagnostics,
373
- });
374
- }
375
- return entries;
149
+ resourceCount: 0,
150
+ })),
151
+ ...project.map((spec) => ({
152
+ spec,
153
+ scope: "project",
154
+ enabled: true,
155
+ resourceCount: 0,
156
+ })),
157
+ ];
376
158
  }