@tea-agent/loop-agent 0.35.0 → 0.35.1-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.
- package/AGENTS.md +108 -108
- package/CHANGELOG.md +30 -0
- package/README.md +165 -165
- package/bin/agent-worker.js +0 -0
- package/bin/loop-agent.js +21 -21
- package/dist/application/task-lifecycle/advance.js +1 -0
- package/dist/commands/cursor-prompt.js +6 -6
- package/dist/commands/init-upgrade.js +351 -19
- package/dist/commands/init.js +14 -67
- package/dist/commands/loop-benchmark.js +11 -11
- package/dist/commands/pi-reuse-benchmark.js +16 -16
- package/dist/commands/run-dag-progress.js +14 -0
- package/dist/commands/task-advance.js +33 -3
- package/dist/shared/operator/capabilities.js +38 -1
- package/dist/sidecars/cursor-prompt/executor.js +1 -1
- package/dist/worker/console/chat/pi-runtime.js +41 -25
- package/dist/worker/console/chat/routes.js +27 -4
- package/dist/worker/console/operation-runner.js +24 -0
- package/dist/worker/console/operation-wait.js +241 -0
- package/dist/worker/console/operator-actions.js +58 -0
- package/dist/worker/console/static/assets/{index-hJqCPs_g.css → index-Dups4sSM.css} +1 -1
- package/dist/worker/console/static/assets/index-SjjjZnV3.js +56 -0
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/operator-chat/slash-palette-nav.js +141 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +13 -2
- package/dist/worker/console/static-src/operator-chat/useComposer.js +30 -7
- package/dist/worker/observe/static/copy.js +67 -67
- package/dist/worker/observe/static/dag-layout.d.ts +36 -36
- package/dist/worker/observe/static/dom.js +220 -220
- package/dist/worker/observe/static/relations.js +133 -133
- package/dist/worker/observe/static/run-processing.js +148 -148
- package/dist/worker/observe/static/views/batch.js +227 -227
- package/dist/worker/observe/static/views/failures.js +143 -143
- package/dist/worker/observe/static/views/feature.js +492 -492
- package/dist/worker/observe/static/views/run.js +453 -453
- package/dist/worker/observe/static/views/shell.js +7 -7
- package/dist/worker/observe/static/views/timeline.js +163 -163
- package/dist/workflows/dag/canvas-observer.js +275 -275
- package/docs/architecture/evolution.md +73 -73
- package/docs/architecture/system-overview.md +100 -100
- package/docs/architecture/worker-and-feature.md +122 -122
- package/docs/skills/README.md +7 -7
- package/docs/templates/adr.md +60 -60
- package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -94
- package/docs/templates/agent-dag-decision-envelope.schema.json +213 -213
- package/docs/templates/agent-dag-decision-gate.prompt.md +246 -246
- package/docs/templates/agent-dag-process-supervisor.prompt.md +98 -98
- package/docs/templates/agent-dag-report.schema.json +473 -473
- package/docs/templates/agent-dag-review-verdict.prompt.md +68 -68
- package/docs/templates/backend-test-result.schema.json +99 -99
- package/docs/templates/evaluation/agents-map-slim-v1.md +87 -87
- package/docs/templates/evaluation/agents-map-verbose-v0.md +153 -153
- package/docs/templates/feature-spec.md +53 -53
- package/docs/templates/frontend-design-contract.md +42 -42
- package/docs/templates/frontend-eval/fixtures/failures/01-type-build-error.md +17 -17
- package/docs/templates/frontend-eval/fixtures/failures/02-unit-component-test-fail.md +16 -16
- package/docs/templates/frontend-eval/fixtures/failures/03-fixture-schema-drift.md +16 -16
- package/docs/templates/frontend-eval/fixtures/failures/04-missing-loading-empty-error-state.md +16 -16
- package/docs/templates/frontend-eval/fixtures/failures/05-forbidden-write-writeset-expansion.md +16 -16
- package/docs/templates/frontend-eval/fixtures/failures/06-unapproved-dependency-add.md +16 -16
- package/docs/templates/frontend-eval/fixtures/failures/07-mock-production-on.md +21 -21
- package/docs/templates/frontend-eval/fixtures/functional/01-simple-component-style.md +29 -29
- package/docs/templates/frontend-eval/fixtures/functional/02-form-validation.md +28 -28
- package/docs/templates/frontend-eval/fixtures/functional/03-list-detail-page.md +28 -28
- package/docs/templates/frontend-eval/fixtures/functional/04-api-mock.md +29 -29
- package/docs/templates/frontend-eval/fixtures/functional/05-permission-auth-gated-ui.md +27 -27
- package/docs/templates/frontend-eval/fixtures/functional/06-ssr-server-client-boundary.md +28 -28
- package/docs/templates/frontend-eval/fixtures/functional/07-shared-public-component-api.md +28 -28
- package/docs/templates/frontend-eval/fixtures/functional/08-pure-local-no-remote.md +27 -27
- package/docs/templates/frontend-eval/metrics.md +138 -138
- package/docs/templates/frontend-eval/smoke-targets.md +53 -53
- package/docs/templates/frontend-task-constraints.md +35 -35
- package/docs/templates/frontend-task-requirement.md +70 -70
- package/docs/templates/init-evolution-review.md +35 -35
- package/docs/templates/init-managed-agents.md +156 -154
- package/docs/templates/interactive-ui-round2-experiment.md +66 -66
- package/docs/templates/knowledge-graph-bootstrap-dag.json +118 -118
- package/docs/templates/knowledge-sync-dag.json +178 -178
- package/docs/templates/knowledge-sync-draft.schema.json +71 -71
- package/docs/templates/product-line/closeout.yaml +9 -9
- package/docs/templates/product-line/design.md +13 -13
- package/docs/templates/product-line/links.md +10 -10
- package/docs/templates/product-line/requirement.md +17 -17
- package/docs/templates/product-line/test-plan.md +7 -7
- package/docs/templates/project-start-checklist.md +9 -9
- package/docs/templates/qa-report.md +48 -48
- package/docs/templates/sprint-contract.md +29 -29
- package/docs/templates/worker-dogfood-evidence.md +80 -80
- package/docs/templates/worker-dogfood-setup.md +68 -68
- package/harness.json +5 -2
- package/package.json +1 -1
- package/scripts/kb-bootstrap-init-skeleton.sh +0 -0
- package/scripts/kb-graph-incremental-prepare.mjs +0 -0
- package/scripts/kb-graph-materialize.mjs +105 -105
- package/scripts/kb-graph-promote.mjs +164 -164
- package/scripts/kb-query.mjs +554 -554
- package/skills/agent-worker/SKILL.md +48 -48
- package/skills/agent-worker/references/agent-worker-operator.md +159 -159
- package/skills/ai-engineering-context/SKILL.md +48 -48
- package/skills/analyze-product-dependencies/scripts/test-validators.mjs +0 -0
- package/skills/analyze-product-dependencies/scripts/validate-api-documentation.mjs +0 -0
- package/skills/analyze-product-dependencies/scripts/validate-dependency-analysis.mjs +0 -0
- package/skills/analyze-product-dependencies/scripts/validate-product-requirement-input.mjs +0 -0
- package/skills/analyze-product-requirements/scripts/compute-source-identity.mjs +0 -0
- package/skills/analyze-product-requirements/scripts/test-validators.mjs +0 -0
- package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +0 -0
- package/skills/analyze-product-requirements/scripts/validate-product-requirement.mjs +0 -0
- package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +0 -0
- package/skills/browser-tools/browser-content.js +103 -103
- package/skills/browser-tools/browser-cookies.js +35 -35
- package/skills/browser-tools/browser-eval.js +53 -53
- package/skills/browser-tools/browser-hn-scraper.js +108 -108
- package/skills/browser-tools/browser-nav.js +44 -44
- package/skills/browser-tools/browser-pick.js +162 -162
- package/skills/browser-tools/browser-screenshot.js +34 -34
- package/skills/browser-tools/browser-start.js +86 -86
- package/skills/browser-tools/package-lock.json +2556 -2556
- package/skills/browser-tools/package.json +19 -19
- package/skills/code-review-core/SKILL.md +20 -20
- package/skills/codebase-scout/SKILL.md +19 -19
- package/skills/grill-me/SKILL.md +10 -10
- package/skills/local-jacoco-coverage/scripts/run-coverage-analysis.sh +0 -0
- package/skills/local-jacoco-coverage/scripts/start-jacoco-agent.sh +0 -0
- package/skills/loop-agent/SKILL.md +1 -0
- package/skills/loop-agent/references/command-reference.md +641 -639
- package/skills/loop-agent/references/docs-converge.md +126 -126
- package/skills/loop-agent/references/learned/README.md +21 -21
- package/skills/loop-agent/references/pi-prompt.md +23 -23
- package/skills/loop-agent/references/pi-subagent-assisted-mode.md +84 -84
- package/skills/playwright-cli/references/element-attributes.md +23 -23
- package/skills/playwright-cli/references/playwright-tests.md +39 -39
- package/skills/playwright-cli/references/request-mocking.md +87 -87
- package/skills/playwright-cli/references/running-code.md +241 -241
- package/skills/playwright-cli/references/session-management.md +225 -225
- package/skills/playwright-cli/references/storage-state.md +275 -275
- package/skills/playwright-cli/references/test-generation.md +433 -433
- package/skills/requesting-code-review/SKILL.md +101 -101
- package/skills/requesting-code-review/code-reviewer.md +168 -168
- package/skills/systematic-debugging/CREATION-LOG.md +119 -119
- package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -158
- package/skills/systematic-debugging/condition-based-waiting.md +115 -115
- package/skills/systematic-debugging/defense-in-depth.md +122 -122
- package/skills/systematic-debugging/find-polluter.sh +63 -63
- package/skills/systematic-debugging/root-cause-tracing.md +169 -169
- package/skills/systematic-debugging/test-academic.md +14 -14
- package/skills/systematic-debugging/test-pressure-1.md +58 -58
- package/skills/systematic-debugging/test-pressure-2.md +68 -68
- package/skills/systematic-debugging/test-pressure-3.md +69 -69
- package/skills/using-git-worktrees/SKILL.md +215 -215
- package/skills/verification-before-completion/SKILL.md +154 -154
- package/skills/webapp-testing/SKILL.md +19 -19
- package/dist/worker/console/static/assets/index-fsjzREob.js +0 -56
package/bin/loop-agent.js
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
4
|
-
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
|
-
|
|
6
|
-
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
7
|
-
const cliEntry = join(packageRoot, "dist", "cli.js");
|
|
8
|
-
|
|
9
|
-
if (!existsSync(cliEntry)) {
|
|
10
|
-
console.error(
|
|
11
|
-
`loop-agent: cannot find built CLI at ${cliEntry}. Run \`npm run build\` before using the package bin.`,
|
|
12
|
-
);
|
|
13
|
-
process.exit(1);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
try {
|
|
17
|
-
await import(pathToFileURL(cliEntry).href);
|
|
18
|
-
} catch (error) {
|
|
19
|
-
console.error(error instanceof Error ? error.message : String(error));
|
|
20
|
-
process.exit(1);
|
|
21
|
-
}
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
|
+
|
|
6
|
+
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
7
|
+
const cliEntry = join(packageRoot, "dist", "cli.js");
|
|
8
|
+
|
|
9
|
+
if (!existsSync(cliEntry)) {
|
|
10
|
+
console.error(
|
|
11
|
+
`loop-agent: cannot find built CLI at ${cliEntry}. Run \`npm run build\` before using the package bin.`,
|
|
12
|
+
);
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
await import(pathToFileURL(cliEntry).href);
|
|
18
|
+
} catch (error) {
|
|
19
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
@@ -1031,6 +1031,7 @@ export async function advanceTaskLifecycle(input) {
|
|
|
1031
1031
|
cwd: input.repoRoot,
|
|
1032
1032
|
initOnly: false,
|
|
1033
1033
|
dryRun: false,
|
|
1034
|
+
...(input.observer ? { observer: input.observer } : {}),
|
|
1034
1035
|
...(input.maxConcurrent !== undefined
|
|
1035
1036
|
? { maxConcurrent: input.maxConcurrent }
|
|
1036
1037
|
: {}),
|
|
@@ -141,7 +141,7 @@ async function runCursorPromptBatch(task, cwd, model, timeoutMs) {
|
|
|
141
141
|
}
|
|
142
142
|
async function runCursorPromptStreaming(task, cwd, model, timeoutMs) {
|
|
143
143
|
const startedAt = Date.now();
|
|
144
|
-
process.stderr.write(`[cursor-prompt] streaming (model=${model}, cwd=${cwd})
|
|
144
|
+
process.stderr.write(`[cursor-prompt] streaming (model=${model}, cwd=${cwd})
|
|
145
145
|
`);
|
|
146
146
|
const runDir = (await computeRunDir(cwd, task)) ?? undefined;
|
|
147
147
|
const result = await executeCursorPromptStream({
|
|
@@ -164,15 +164,15 @@ async function runCursorPromptStreaming(task, cwd, model, timeoutMs) {
|
|
|
164
164
|
});
|
|
165
165
|
const elapsed = ((Date.now() - startedAt) / 1000).toFixed(1);
|
|
166
166
|
if (result.ok) {
|
|
167
|
-
process.stderr.write(`
|
|
168
|
-
[cursor-prompt] done in ${elapsed}s, status=${result.status}
|
|
167
|
+
process.stderr.write(`
|
|
168
|
+
[cursor-prompt] done in ${elapsed}s, status=${result.status}
|
|
169
169
|
`);
|
|
170
170
|
}
|
|
171
171
|
else {
|
|
172
172
|
const stderr = result.stderr || "(no output)";
|
|
173
|
-
process.stderr.write(`
|
|
174
|
-
[cursor-prompt] FAILED in ${elapsed}s (${result.failureCategory}):
|
|
175
|
-
${stderr}
|
|
173
|
+
process.stderr.write(`
|
|
174
|
+
[cursor-prompt] FAILED in ${elapsed}s (${result.failureCategory}):
|
|
175
|
+
${stderr}
|
|
176
176
|
`);
|
|
177
177
|
process.exit(1);
|
|
178
178
|
}
|
|
@@ -46,6 +46,60 @@ function isHomeFingerprint(value) {
|
|
|
46
46
|
keys[1] === "sha256" &&
|
|
47
47
|
isSha256(value.sha256));
|
|
48
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Shape guard for the controller-owned gitignore migration assessment.
|
|
51
|
+
* `assessGitignoreMigration` is the single schema fact source; status/report
|
|
52
|
+
* reads fail closed on any shape that the writer could not have produced
|
|
53
|
+
* (AC-005). `queryError` may be absent and arrays may be empty; extra keys are
|
|
54
|
+
* tolerated for forward compatibility.
|
|
55
|
+
*/
|
|
56
|
+
function isGitignoreMigrationAssessment(value) {
|
|
57
|
+
if (!isRecord(value))
|
|
58
|
+
return false;
|
|
59
|
+
const assessment = value;
|
|
60
|
+
if (assessment.schemaVersion !== 1)
|
|
61
|
+
return false;
|
|
62
|
+
if (typeof assessment.managedBlockRefreshed !== "boolean")
|
|
63
|
+
return false;
|
|
64
|
+
if (typeof assessment.needsAgentsReview !== "boolean")
|
|
65
|
+
return false;
|
|
66
|
+
if (!isRecord(assessment.git))
|
|
67
|
+
return false;
|
|
68
|
+
const git = assessment.git;
|
|
69
|
+
if (typeof git.isRepo !== "boolean")
|
|
70
|
+
return false;
|
|
71
|
+
if (git.queryError !== undefined && typeof git.queryError !== "string") {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
if (!Array.isArray(assessment.harnessTracked) ||
|
|
75
|
+
!assessment.harnessTracked.every((entry) => typeof entry === "string")) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
if (!Array.isArray(assessment.agentsTracked) ||
|
|
79
|
+
!assessment.agentsTracked.every((entry) => typeof entry === "string")) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
if (!Array.isArray(assessment.stagedRisk) ||
|
|
83
|
+
!assessment.stagedRisk.every((entry) => typeof entry === "string")) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
if (!Array.isArray(assessment.followUp) ||
|
|
87
|
+
!assessment.followUp.every((entry) => typeof entry === "string")) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
if (!Array.isArray(assessment.recommendedCommands))
|
|
91
|
+
return false;
|
|
92
|
+
for (const entry of assessment.recommendedCommands) {
|
|
93
|
+
if (!isRecord(entry))
|
|
94
|
+
return false;
|
|
95
|
+
if (typeof entry.command !== "string" || typeof entry.note !== "string") {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return (assessment.status === "clean" ||
|
|
100
|
+
assessment.status === "recommended" ||
|
|
101
|
+
assessment.status === "blocked");
|
|
102
|
+
}
|
|
49
103
|
function cloneJson(value) {
|
|
50
104
|
return JSON.parse(JSON.stringify(value));
|
|
51
105
|
}
|
|
@@ -171,6 +225,143 @@ function assertTerminalCoherence(state) {
|
|
|
171
225
|
throw new Error("invalid init upgrade status/phase coherence");
|
|
172
226
|
}
|
|
173
227
|
}
|
|
228
|
+
/** Read-only git query; never mutates the user index (no status refresh, no write). */
|
|
229
|
+
function runGitQuery(cwd, args) {
|
|
230
|
+
return new Promise((resolve) => {
|
|
231
|
+
const child = spawn("git", ["-C", cwd, ...args], {
|
|
232
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
233
|
+
});
|
|
234
|
+
const stdoutChunks = [];
|
|
235
|
+
const stderrChunks = [];
|
|
236
|
+
child.stdout?.on("data", (chunk) => stdoutChunks.push(chunk));
|
|
237
|
+
child.stderr?.on("data", (chunk) => stderrChunks.push(chunk));
|
|
238
|
+
child.on("error", (error) => {
|
|
239
|
+
resolve({ ok: false, stdout: "", stderr: error.message });
|
|
240
|
+
});
|
|
241
|
+
child.on("close", (code) => {
|
|
242
|
+
resolve({
|
|
243
|
+
ok: code === 0,
|
|
244
|
+
stdout: Buffer.concat(stdoutChunks).toString("utf8"),
|
|
245
|
+
stderr: Buffer.concat(stderrChunks).toString("utf8"),
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
function splitNulPaths(output) {
|
|
251
|
+
return output.split("\0").filter((entry) => entry.length > 0);
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Gitignore migration assessment (PRD 2026-08-13): inventories tracked
|
|
255
|
+
* `.harness/**` / `.agents/**` with read-only git queries after the managed
|
|
256
|
+
* block has converged, and recommends index-only untrack commands. The
|
|
257
|
+
* controller never mutates the user Git index and never deletes local files.
|
|
258
|
+
*/
|
|
259
|
+
export async function assessGitignoreMigration(repoRoot, input) {
|
|
260
|
+
const base = {
|
|
261
|
+
schemaVersion: 1,
|
|
262
|
+
managedBlockRefreshed: input.managedBlockRefreshed,
|
|
263
|
+
harnessTracked: [],
|
|
264
|
+
agentsTracked: [],
|
|
265
|
+
stagedRisk: [],
|
|
266
|
+
needsAgentsReview: false,
|
|
267
|
+
followUp: [
|
|
268
|
+
"run `git status --short` and confirm only the intended untrack changes",
|
|
269
|
+
"confirm the .harness/ and .agents/ directories still exist locally after untracking",
|
|
270
|
+
"run `loop-agent init doctor` to re-check the gitignore loop-agent block",
|
|
271
|
+
],
|
|
272
|
+
};
|
|
273
|
+
const inside = await runGitQuery(repoRoot, ["rev-parse", "--is-inside-work-tree"]);
|
|
274
|
+
if (!inside.ok) {
|
|
275
|
+
return {
|
|
276
|
+
...base,
|
|
277
|
+
git: {
|
|
278
|
+
isRepo: false,
|
|
279
|
+
queryError: inside.stderr.trim() || "not inside a Git work tree",
|
|
280
|
+
},
|
|
281
|
+
recommendedCommands: [],
|
|
282
|
+
status: "blocked",
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
const tracked = await runGitQuery(repoRoot, [
|
|
286
|
+
"ls-files",
|
|
287
|
+
"-z",
|
|
288
|
+
"--cached",
|
|
289
|
+
"--",
|
|
290
|
+
".harness",
|
|
291
|
+
".agents",
|
|
292
|
+
]);
|
|
293
|
+
if (!tracked.ok) {
|
|
294
|
+
return {
|
|
295
|
+
...base,
|
|
296
|
+
git: {
|
|
297
|
+
isRepo: true,
|
|
298
|
+
queryError: tracked.stderr.trim() || "git ls-files query failed",
|
|
299
|
+
},
|
|
300
|
+
recommendedCommands: [],
|
|
301
|
+
status: "blocked",
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
const staged = await runGitQuery(repoRoot, [
|
|
305
|
+
"diff",
|
|
306
|
+
"--cached",
|
|
307
|
+
"--name-only",
|
|
308
|
+
"-z",
|
|
309
|
+
]);
|
|
310
|
+
if (!staged.ok) {
|
|
311
|
+
return {
|
|
312
|
+
...base,
|
|
313
|
+
git: {
|
|
314
|
+
isRepo: true,
|
|
315
|
+
queryError: staged.stderr.trim() || "git diff --cached query failed",
|
|
316
|
+
},
|
|
317
|
+
recommendedCommands: [],
|
|
318
|
+
status: "blocked",
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
const trackedPaths = splitNulPaths(tracked.stdout);
|
|
322
|
+
const harnessTracked = trackedPaths.filter((entry) => entry.startsWith(".harness/"));
|
|
323
|
+
const agentsTracked = trackedPaths.filter((entry) => entry.startsWith(".agents/"));
|
|
324
|
+
const stagedRisk = splitNulPaths(staged.stdout);
|
|
325
|
+
const needsAgentsReview = agentsTracked.length > 0;
|
|
326
|
+
if (stagedRisk.length > 0) {
|
|
327
|
+
// Fail closed: staged entries make any untrack advice ambiguous.
|
|
328
|
+
return {
|
|
329
|
+
...base,
|
|
330
|
+
git: { isRepo: true },
|
|
331
|
+
harnessTracked,
|
|
332
|
+
agentsTracked,
|
|
333
|
+
stagedRisk,
|
|
334
|
+
needsAgentsReview,
|
|
335
|
+
recommendedCommands: [],
|
|
336
|
+
status: "blocked",
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
const recommendedCommands = [];
|
|
340
|
+
if (harnessTracked.length > 0) {
|
|
341
|
+
recommendedCommands.push({
|
|
342
|
+
command: "git rm -r --cached --ignore-unmatch .harness",
|
|
343
|
+
note: "untracks tracked .harness runtime facts; index-only, working-tree files are kept",
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
if (agentsTracked.length > 0) {
|
|
347
|
+
recommendedCommands.push({
|
|
348
|
+
command: "git rm -r --cached --ignore-unmatch .agents",
|
|
349
|
+
note: "untracks tracked .agents local skills; index-only, working-tree files are kept; review team-shareable customizations first",
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
return {
|
|
353
|
+
...base,
|
|
354
|
+
git: { isRepo: true },
|
|
355
|
+
harnessTracked,
|
|
356
|
+
agentsTracked,
|
|
357
|
+
stagedRisk,
|
|
358
|
+
needsAgentsReview,
|
|
359
|
+
recommendedCommands,
|
|
360
|
+
status: harnessTracked.length > 0 || agentsTracked.length > 0
|
|
361
|
+
? "recommended"
|
|
362
|
+
: "clean",
|
|
363
|
+
};
|
|
364
|
+
}
|
|
174
365
|
function runRoot(repoRoot) {
|
|
175
366
|
return path.join(repoRoot, RUN_ROOT);
|
|
176
367
|
}
|
|
@@ -240,6 +431,7 @@ async function writeControllerAnchor(input) {
|
|
|
240
431
|
throw new Error("init upgrade controller authority anchor path escapes Git directory");
|
|
241
432
|
}
|
|
242
433
|
let authoritativeHomeFingerprint;
|
|
434
|
+
let authoritativeGitignoreMigrationSha256;
|
|
243
435
|
if (input.initialize) {
|
|
244
436
|
if (await exists(location.file)) {
|
|
245
437
|
throw new Error("init upgrade controller authority anchor already exists");
|
|
@@ -252,6 +444,14 @@ async function writeControllerAnchor(input) {
|
|
|
252
444
|
else {
|
|
253
445
|
const current = await readControllerAnchor(location.repoRoot, input.state);
|
|
254
446
|
authoritativeHomeFingerprint = current.homeFingerprint;
|
|
447
|
+
// Preserve the gitignore migration artifact binding across later
|
|
448
|
+
// mergeAuthority/completion writes; fail closed on a corrupt or
|
|
449
|
+
// non-SHA-256 binding instead of silently dropping or propagating it.
|
|
450
|
+
const digest = input.gitignoreMigrationSha256 ?? current.gitignoreMigrationSha256;
|
|
451
|
+
if (digest !== undefined && !isSha256(digest)) {
|
|
452
|
+
throw new Error("init upgrade controller authority gitignore migration digest binding is invalid");
|
|
453
|
+
}
|
|
454
|
+
authoritativeGitignoreMigrationSha256 = digest;
|
|
255
455
|
}
|
|
256
456
|
await writeJsonAtomic(location.file, {
|
|
257
457
|
schemaVersion: 1,
|
|
@@ -259,6 +459,9 @@ async function writeControllerAnchor(input) {
|
|
|
259
459
|
runId: input.state.runId,
|
|
260
460
|
controllerIdentity: input.state.controllerIdentity,
|
|
261
461
|
homeFingerprint: authoritativeHomeFingerprint,
|
|
462
|
+
...(authoritativeGitignoreMigrationSha256 !== undefined
|
|
463
|
+
? { gitignoreMigrationSha256: authoritativeGitignoreMigrationSha256 }
|
|
464
|
+
: {}),
|
|
262
465
|
...(input.mergeAuthority ? { mergeAuthority: input.mergeAuthority } : {}),
|
|
263
466
|
...(input.completion ? { completion: input.completion } : {}),
|
|
264
467
|
});
|
|
@@ -451,7 +654,7 @@ async function saveState(directory, state) {
|
|
|
451
654
|
state.updatedAt = new Date().toISOString();
|
|
452
655
|
await writeJsonAtomic(path.join(directory, "state.json"), state);
|
|
453
656
|
}
|
|
454
|
-
function toResult(state, verification) {
|
|
657
|
+
function toResult(state, verification, gitignoreMigration) {
|
|
455
658
|
return {
|
|
456
659
|
runId: state.runId,
|
|
457
660
|
status: state.status,
|
|
@@ -462,9 +665,10 @@ function toResult(state, verification) {
|
|
|
462
665
|
humanDecisions: state.humanDecisions,
|
|
463
666
|
nextAction: state.nextAction,
|
|
464
667
|
...(verification ? { verification } : {}),
|
|
668
|
+
...(gitignoreMigration ? { gitignoreMigration } : {}),
|
|
465
669
|
};
|
|
466
670
|
}
|
|
467
|
-
function renderReport(state, verification) {
|
|
671
|
+
function renderReport(state, verification, gitignoreMigration) {
|
|
468
672
|
const lines = [
|
|
469
673
|
"# loop-agent init upgrade report",
|
|
470
674
|
"",
|
|
@@ -480,6 +684,47 @@ function renderReport(state, verification) {
|
|
|
480
684
|
`- retired path actions: ${state.actionReceipts.filter((action) => action.type === "migrate-owned-file" || action.type === "remove-owned-file" || action.type === "remove-empty-directory").length}`,
|
|
481
685
|
`- remaining model merge tasks: ${state.mergeTasks.length}`,
|
|
482
686
|
`- remaining human decisions: ${state.humanDecisions.length}`,
|
|
687
|
+
`- gitignore migration assessment: \`.harness/init-upgrades/${state.runId}/gitignore-migration.json\` (index-only untrack guidance; the controller never modifies the Git index)`,
|
|
688
|
+
...(gitignoreMigration
|
|
689
|
+
? [
|
|
690
|
+
"",
|
|
691
|
+
"## Gitignore migration assessment",
|
|
692
|
+
"",
|
|
693
|
+
`- status: \`${gitignoreMigration.status}\``,
|
|
694
|
+
`- managed block refreshed: \`${gitignoreMigration.managedBlockRefreshed}\``,
|
|
695
|
+
`- tracked \`.harness\` inventory: ${gitignoreMigration.harnessTracked.length > 0
|
|
696
|
+
? gitignoreMigration.harnessTracked
|
|
697
|
+
.map((entry) => `\`${entry}\``)
|
|
698
|
+
.join(", ")
|
|
699
|
+
: "(none)"}`,
|
|
700
|
+
`- tracked \`.agents\` inventory: ${gitignoreMigration.agentsTracked.length > 0
|
|
701
|
+
? gitignoreMigration.agentsTracked
|
|
702
|
+
.map((entry) => `\`${entry}\``)
|
|
703
|
+
.join(", ")
|
|
704
|
+
: "(none)"}`,
|
|
705
|
+
...(gitignoreMigration.stagedRisk.length > 0
|
|
706
|
+
? [
|
|
707
|
+
`- staged risk: ${gitignoreMigration.stagedRisk
|
|
708
|
+
.map((entry) => `\`${entry}\``)
|
|
709
|
+
.join(", ")}`,
|
|
710
|
+
]
|
|
711
|
+
: []),
|
|
712
|
+
...(gitignoreMigration.git.queryError
|
|
713
|
+
? [
|
|
714
|
+
`- git query blocker: \`${gitignoreMigration.git.queryError}\``,
|
|
715
|
+
]
|
|
716
|
+
: []),
|
|
717
|
+
`- \`.agents\` review required: \`${gitignoreMigration.needsAgentsReview}\``,
|
|
718
|
+
"- recommended commands:",
|
|
719
|
+
...(gitignoreMigration.recommendedCommands.length > 0
|
|
720
|
+
? gitignoreMigration.recommendedCommands.map((entry) => ` - \`${entry.command}\` — ${entry.note}`)
|
|
721
|
+
: [" - (none)"]),
|
|
722
|
+
"- follow-up:",
|
|
723
|
+
...(gitignoreMigration.followUp.length > 0
|
|
724
|
+
? gitignoreMigration.followUp.map((entry) => ` - ${entry}`)
|
|
725
|
+
: [" - (none)"]),
|
|
726
|
+
]
|
|
727
|
+
: []),
|
|
483
728
|
"",
|
|
484
729
|
"## Next action",
|
|
485
730
|
"",
|
|
@@ -501,8 +746,8 @@ function renderReport(state, verification) {
|
|
|
501
746
|
];
|
|
502
747
|
return `${lines.join("\n")}\n`;
|
|
503
748
|
}
|
|
504
|
-
async function writeReport(directory, state, verification) {
|
|
505
|
-
await writeFile(path.join(directory, "final-report.md"), renderReport(state, verification), "utf-8");
|
|
749
|
+
async function writeReport(directory, state, verification, gitignoreMigration) {
|
|
750
|
+
await writeFile(path.join(directory, "final-report.md"), renderReport(state, verification, gitignoreMigration), "utf-8");
|
|
506
751
|
}
|
|
507
752
|
async function assertInactive(input) {
|
|
508
753
|
let activity;
|
|
@@ -2303,14 +2548,62 @@ async function readValidatedStatusState(repoRoot, runId) {
|
|
|
2303
2548
|
throw new Error("Controller identity drift detected for persisted init upgrade state");
|
|
2304
2549
|
}
|
|
2305
2550
|
}
|
|
2306
|
-
return { state, directory };
|
|
2551
|
+
return { state, directory, anchor };
|
|
2552
|
+
}
|
|
2553
|
+
/**
|
|
2554
|
+
* Read-only projection of the controller-owned gitignore migration assessment
|
|
2555
|
+
* for status/report reads. The artifact is written by executeUpgrade after the
|
|
2556
|
+
* managed block has converged; a missing artifact is tolerated only while the
|
|
2557
|
+
* run has not yet reached that write point (DISCOVER/VERSION_GATE). Every
|
|
2558
|
+
* other missing, malformed, or shape-invalid artifact fails closed so
|
|
2559
|
+
* status/report never trust mutable or incomplete content (AC-005).
|
|
2560
|
+
*/
|
|
2561
|
+
async function readGitignoreMigrationProjection(directory, state, gitignoreMigrationSha256) {
|
|
2562
|
+
const file = path.join(directory, "gitignore-migration.json");
|
|
2563
|
+
let rawBytes;
|
|
2564
|
+
let raw;
|
|
2565
|
+
try {
|
|
2566
|
+
rawBytes = await readFile(file);
|
|
2567
|
+
raw = rawBytes.toString("utf-8");
|
|
2568
|
+
}
|
|
2569
|
+
catch (error) {
|
|
2570
|
+
const cause = error;
|
|
2571
|
+
if (cause.code === "ENOENT") {
|
|
2572
|
+
if (state.phase === "DISCOVER" || state.phase === "VERSION_GATE") {
|
|
2573
|
+
return undefined;
|
|
2574
|
+
}
|
|
2575
|
+
throw new Error(`init upgrade integrity error: gitignore migration artifact is missing for run ${state.runId} (phase ${state.phase}); refusing to trust an incomplete run`);
|
|
2576
|
+
}
|
|
2577
|
+
throw error;
|
|
2578
|
+
}
|
|
2579
|
+
let parsed;
|
|
2580
|
+
try {
|
|
2581
|
+
parsed = JSON.parse(raw);
|
|
2582
|
+
}
|
|
2583
|
+
catch {
|
|
2584
|
+
throw new Error(`init upgrade integrity error: gitignore migration artifact is malformed JSON for run ${state.runId} (${file})`);
|
|
2585
|
+
}
|
|
2586
|
+
if (!isGitignoreMigrationAssessment(parsed)) {
|
|
2587
|
+
throw new Error(`init upgrade integrity error: gitignore migration artifact has an invalid shape for run ${state.runId} (${file})`);
|
|
2588
|
+
}
|
|
2589
|
+
// The artifact exists, so this run has generated an assessment; the
|
|
2590
|
+
// controller authority binding must exist, be a legal SHA-256, and match
|
|
2591
|
+
// the raw artifact bytes on disk (fail closed otherwise).
|
|
2592
|
+
if (gitignoreMigrationSha256 === undefined || !isSha256(gitignoreMigrationSha256)) {
|
|
2593
|
+
throw new Error(`init upgrade integrity error: gitignore migration artifact digest binding is missing or invalid for run ${state.runId}`);
|
|
2594
|
+
}
|
|
2595
|
+
if (sha256(rawBytes) !== gitignoreMigrationSha256) {
|
|
2596
|
+
throw new Error(`init upgrade integrity error: gitignore migration artifact digest does not match the controller authority binding for run ${state.runId}`);
|
|
2597
|
+
}
|
|
2598
|
+
return parsed;
|
|
2307
2599
|
}
|
|
2308
2600
|
async function readStatusResult(repoRoot, runId) {
|
|
2309
|
-
const { state } = await readValidatedStatusState(repoRoot, runId);
|
|
2310
|
-
if (state.status !== "completed")
|
|
2311
|
-
return toResult(state);
|
|
2601
|
+
const { state, directory, anchor } = await readValidatedStatusState(repoRoot, runId);
|
|
2602
|
+
if (state.status !== "completed") {
|
|
2603
|
+
return toResult(state, undefined, await readGitignoreMigrationProjection(directory, state, anchor.gitignoreMigrationSha256));
|
|
2604
|
+
}
|
|
2312
2605
|
const authenticated = await readFreshCompletedVerification(repoRoot, state);
|
|
2313
|
-
return toResult(state, authenticated.verification);
|
|
2606
|
+
return toResult(state, authenticated.verification, await readGitignoreMigrationProjection(directory, state, anchor.gitignoreMigrationSha256));
|
|
2314
2607
|
}
|
|
2315
2608
|
async function executeUpgrade(directory, state, input) {
|
|
2316
2609
|
const identity = await readIdentity();
|
|
@@ -2445,17 +2738,50 @@ async function executeUpgrade(directory, state, input) {
|
|
|
2445
2738
|
}
|
|
2446
2739
|
}
|
|
2447
2740
|
state.humanDecisions = post.humanDecisions;
|
|
2741
|
+
// Gitignore migration assessment: after the managed block has converged,
|
|
2742
|
+
// inventory tracked .harness/.agents paths with read-only git queries and
|
|
2743
|
+
// recommend index-only untrack commands. The controller never mutates the
|
|
2744
|
+
// user Git index; suggested commands only change the index and keep the
|
|
2745
|
+
// working tree intact.
|
|
2746
|
+
const gitignoreMigration = await assessGitignoreMigration(state.repoRoot, {
|
|
2747
|
+
managedBlockRefreshed: !post.deterministicActions.some((action) => action.type === "refresh-managed-block" &&
|
|
2748
|
+
action.path === ".gitignore"),
|
|
2749
|
+
});
|
|
2750
|
+
await writeJsonAtomic(path.join(directory, "gitignore-migration.json"), gitignoreMigration);
|
|
2751
|
+
// Bind the raw on-disk artifact bytes to the Git-dir controller authority
|
|
2752
|
+
// before any needs-human-decision/model-merge/completed return; later
|
|
2753
|
+
// mergeAuthority/completion anchor writes preserve this binding.
|
|
2754
|
+
await writeControllerAnchor({
|
|
2755
|
+
state,
|
|
2756
|
+
gitignoreMigrationSha256: sha256(await readFile(path.join(directory, "gitignore-migration.json"))),
|
|
2757
|
+
});
|
|
2758
|
+
if (gitignoreMigration.status === "blocked" ||
|
|
2759
|
+
gitignoreMigration.needsAgentsReview) {
|
|
2760
|
+
// Route through the existing needs-human-decision pause: `.agents`
|
|
2761
|
+
// tracked content must be reviewed (PRD §7) and staged blockers must be
|
|
2762
|
+
// resolved before safe untrack guidance can be given (PRD §8).
|
|
2763
|
+
post.humanDecisions.push(gitignoreMigration.status === "blocked"
|
|
2764
|
+
? {
|
|
2765
|
+
path: ".harness/** (gitignore migration)",
|
|
2766
|
+
reason: "staged changes or a failed git query block safe untrack guidance; resolve the blocker, then rerun the migration assessment",
|
|
2767
|
+
}
|
|
2768
|
+
: {
|
|
2769
|
+
path: ".agents/** (gitignore migration)",
|
|
2770
|
+
reason: "tracked .agents content may include team-shareable customizations; review before running the suggested untrack commands",
|
|
2771
|
+
});
|
|
2772
|
+
}
|
|
2773
|
+
state.humanDecisions = post.humanDecisions;
|
|
2448
2774
|
if (post.humanDecisions.length > 0) {
|
|
2449
2775
|
state.mergeTasks = [];
|
|
2450
2776
|
state.mergeGuard = undefined;
|
|
2451
2777
|
state.status = "needs-human-decision";
|
|
2452
2778
|
state.phase = "MODEL_MERGE";
|
|
2453
2779
|
state.nextAction =
|
|
2454
|
-
"Resolve the recorded destructive, credential, or ambiguous decisions; the controller will not overwrite uncertain user intent.";
|
|
2780
|
+
"Resolve the recorded destructive, credential, or ambiguous decisions; the controller will not overwrite uncertain user intent. For gitignore migration: review tracked .agents customizations or resolve the staged blocker first, then rerun the assessment so index-only untrack commands can be recommended.";
|
|
2455
2781
|
await writeJsonAtomic(path.join(directory, "merge-tasks.json"), state.mergeTasks);
|
|
2456
2782
|
await saveState(directory, state);
|
|
2457
|
-
await writeReport(directory, state);
|
|
2458
|
-
return toResult(state);
|
|
2783
|
+
await writeReport(directory, state, undefined, gitignoreMigration);
|
|
2784
|
+
return toResult(state, undefined, gitignoreMigration);
|
|
2459
2785
|
}
|
|
2460
2786
|
if (post.modelMergeTasks.length > 0) {
|
|
2461
2787
|
const activeTask = post.modelMergeTasks[0];
|
|
@@ -2487,8 +2813,8 @@ async function executeUpgrade(directory, state, input) {
|
|
|
2487
2813
|
state.nextAction = `Complete only task ${activeTask.taskId} within allowedPaths=[${activeTask.allowedPaths.join(", ")}], then call init upgrade --continue. The controller will reject every other workspace change and all symlink/realpath escapes.`;
|
|
2488
2814
|
await writeJsonAtomic(path.join(directory, "merge-tasks.json"), state.mergeTasks);
|
|
2489
2815
|
await saveState(directory, state);
|
|
2490
|
-
await writeReport(directory, state);
|
|
2491
|
-
return toResult(state);
|
|
2816
|
+
await writeReport(directory, state, undefined, gitignoreMigration);
|
|
2817
|
+
return toResult(state, undefined, gitignoreMigration);
|
|
2492
2818
|
}
|
|
2493
2819
|
state.mergeTasks = [];
|
|
2494
2820
|
state.mergeGuard = undefined;
|
|
@@ -2504,7 +2830,7 @@ async function executeUpgrade(directory, state, input) {
|
|
|
2504
2830
|
state.nextAction =
|
|
2505
2831
|
"Init upgrade completed; reload the host session so updated AGENTS, skills, and recovery extensions are discovered.";
|
|
2506
2832
|
// The report is controller-produced before its exact bytes are anchored.
|
|
2507
|
-
await writeReport(directory, state, verification);
|
|
2833
|
+
await writeReport(directory, state, verification, gitignoreMigration);
|
|
2508
2834
|
const finalReportSha256 = sha256(await readFile(path.join(directory, "final-report.md")));
|
|
2509
2835
|
await writeControllerAnchor({
|
|
2510
2836
|
state,
|
|
@@ -2529,8 +2855,8 @@ async function executeUpgrade(directory, state, input) {
|
|
|
2529
2855
|
: "Verification failed. Inspect verification.json; repair only the controller-returned concrete merge path or address the reported external blocker, then use --continue.";
|
|
2530
2856
|
}
|
|
2531
2857
|
await saveState(directory, state);
|
|
2532
|
-
await writeReport(directory, state, verification);
|
|
2533
|
-
return toResult(state, verification);
|
|
2858
|
+
await writeReport(directory, state, verification, gitignoreMigration);
|
|
2859
|
+
return toResult(state, verification, gitignoreMigration);
|
|
2534
2860
|
}
|
|
2535
2861
|
export async function runInitUpgrade(input) {
|
|
2536
2862
|
const repoRoot = path.resolve(input.repoRoot);
|
|
@@ -2541,14 +2867,20 @@ export async function runInitUpgrade(input) {
|
|
|
2541
2867
|
if (mode === "status")
|
|
2542
2868
|
return await readStatusResult(repoRoot, input.runId);
|
|
2543
2869
|
if (mode === "report") {
|
|
2544
|
-
const { state } = await readValidatedStatusState(repoRoot, input.runId);
|
|
2870
|
+
const { state, directory, anchor } = await readValidatedStatusState(repoRoot, input.runId);
|
|
2545
2871
|
if (state.status === "completed") {
|
|
2546
2872
|
const authenticated = await readFreshCompletedVerification(repoRoot, state);
|
|
2873
|
+
// Completed report bytes are anchored by finalReportSha256; still
|
|
2874
|
+
// verify the gitignore migration artifact binding so shape-valid
|
|
2875
|
+
// tampering fails closed here as well.
|
|
2876
|
+
await readGitignoreMigrationProjection(directory, state, anchor.gitignoreMigrationSha256);
|
|
2547
2877
|
return { markdown: authenticated.reportMarkdown };
|
|
2548
2878
|
}
|
|
2549
2879
|
// Non-completed reports are projections of the validated state, never an
|
|
2550
2880
|
// unauthenticated read of a mutable run-local report file.
|
|
2551
|
-
return {
|
|
2881
|
+
return {
|
|
2882
|
+
markdown: renderReport(state, undefined, await readGitignoreMigrationProjection(directory, state, anchor.gitignoreMigrationSha256)),
|
|
2883
|
+
};
|
|
2552
2884
|
}
|
|
2553
2885
|
const runId = mode === "continue"
|
|
2554
2886
|
? input.runId
|