@tea-agent/loop-agent 0.28.1 → 0.28.2
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 +1 -1
- package/CHANGELOG.md +25 -0
- package/README.md +11 -1
- package/dist/cli/command-definitions.js +2 -1
- package/dist/commands/client-recovery.js +111 -8
- package/dist/commands/dag-init-hybrid.js +1 -1
- package/dist/commands/init-upgrade.js +2479 -0
- package/dist/commands/init.js +120 -9
- package/dist/governance/manifest-types.js +65 -0
- package/dist/shared/operator/capabilities.js +350 -2
- package/dist/task/worktree.js +256 -39
- package/dist/worker/cli.js +22 -12
- package/dist/worker/console/chat/workspace-landing.js +16 -6
- package/dist/worker/console/observe-health-match.js +2 -0
- package/dist/worker/console/observe-link.js +4 -0
- package/dist/worker/console/operator-actions.js +183 -4
- package/dist/worker/console/operator-selection.js +13 -0
- package/dist/worker/console/static/assets/index-BfRgtLF4.js +29 -0
- package/dist/worker/console/static/index.html +1 -1
- package/dist/worker/observe/health.js +1 -0
- package/dist/worker/observe/night-jobs.js +104 -0
- package/dist/worker/observe/routes.js +48 -0
- package/dist/worker/observe/static/app.js +3 -0
- package/dist/worker/observe/static/constants.js +1 -0
- package/dist/worker/observe/static/index.html +47 -0
- package/dist/worker/observe/static/router.js +10 -0
- package/dist/worker/observe/static/shell-chrome.js +1 -0
- package/dist/worker/observe/static/views/night.js +201 -0
- package/dist/worker/report/morning-report.js +56 -16
- package/dist/worker/run-task/execute-prepared-task.js +153 -0
- package/dist/worker/runner/single-task-attempt.js +147 -0
- package/dist/worker/scheduler/admission.js +536 -0
- package/dist/worker/scheduler/auto-followup.js +99 -0
- package/dist/worker/scheduler/cli.js +539 -0
- package/dist/worker/scheduler/dispatcher.js +503 -0
- package/dist/worker/scheduler/doctor.js +346 -0
- package/dist/worker/scheduler/evidence.js +170 -0
- package/dist/worker/scheduler/git-base.js +52 -0
- package/dist/worker/scheduler/index.js +23 -0
- package/dist/worker/scheduler/lease.js +114 -0
- package/dist/worker/scheduler/lifecycle.js +348 -0
- package/dist/worker/scheduler/lock.js +80 -0
- package/dist/worker/scheduler/morning-window.js +161 -0
- package/dist/worker/scheduler/night-git-finalizer.js +88 -0
- package/dist/worker/scheduler/night-harvest.js +421 -0
- package/dist/worker/scheduler/paths.js +84 -0
- package/dist/worker/scheduler/prepared-attempt-recovery.js +471 -0
- package/dist/worker/scheduler/recovery.js +277 -0
- package/dist/worker/scheduler/reservation.js +146 -0
- package/dist/worker/scheduler/retry.js +199 -0
- package/dist/worker/scheduler/scheduler-loop.js +272 -0
- package/dist/worker/scheduler/store.js +275 -0
- package/dist/worker/scheduler/traceability.js +54 -0
- package/dist/worker/scheduler/trigger.js +258 -0
- package/dist/worker/scheduler/types.js +369 -0
- package/dist/worker/scheduler/workspace-adapter.js +91 -0
- package/docs/architecture/runtime-boundaries.md +9 -0
- package/docs/init-surface.manifest.json +9 -2
- package/docs/templates/harness.schema.json +107 -0
- package/docs/templates/init-managed-agents.md +18 -8
- package/harness.json +22 -0
- package/package.json +1 -1
- package/skills/loop-agent/SKILL.md +28 -36
- package/skills/loop-agent/references/command-reference.md +40 -16
- package/skills/loop-agent/references/hybrid-dag.md +1 -1
- package/dist/worker/console/static/assets/index-CNO7n6qB.js +0 -29
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { access, readFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { controllerIdentityExpectationFailure, resolveControllerIdentity, } from "../loop-agent/loop-agent-client.js";
|
|
5
|
+
import { buildWorkerRunId, } from "./run-task.js";
|
|
6
|
+
/**
|
|
7
|
+
* Consume a frozen night admission: do not materialize/generate DAG.
|
|
8
|
+
* Verifies workspace + gate token, then runs `task advance --approve-gate`
|
|
9
|
+
* with cwd = workspaceRoot.
|
|
10
|
+
*/
|
|
11
|
+
export async function executePreparedTaskSpec(options) {
|
|
12
|
+
const now = options.now ?? new Date();
|
|
13
|
+
const admission = options.admission;
|
|
14
|
+
if (!admission.harnessTaskId) {
|
|
15
|
+
throw new Error("executePreparedTask: admission.harnessTaskId required");
|
|
16
|
+
}
|
|
17
|
+
if (!admission.gateApproval?.token || !admission.gateApproval.approvedAt) {
|
|
18
|
+
throw new Error("executePreparedTask: approved admission gate receipt required");
|
|
19
|
+
}
|
|
20
|
+
if (!admission.workspace?.path) {
|
|
21
|
+
throw new Error("executePreparedTask: admission.workspace required");
|
|
22
|
+
}
|
|
23
|
+
const controllerIdentity = resolveControllerIdentity(options.client, options.controllerIdentity ??
|
|
24
|
+
admission.controllerIdentity);
|
|
25
|
+
const identityFailure = controllerIdentityExpectationFailure(controllerIdentity, options.controllerExpectation);
|
|
26
|
+
if (identityFailure) {
|
|
27
|
+
throw new Error(`${identityFailure.code}: ${identityFailure.message}`);
|
|
28
|
+
}
|
|
29
|
+
// Fail closed: never run on control root when workspace differs.
|
|
30
|
+
const workspaceRoot = path.resolve(options.workspaceRoot);
|
|
31
|
+
const controlRoot = path.resolve(options.controlRepoRoot);
|
|
32
|
+
if (workspaceRoot === controlRoot) {
|
|
33
|
+
throw new Error("executePreparedTask: refusing to run on controlRepoRoot (night isolation required)");
|
|
34
|
+
}
|
|
35
|
+
const expectedWorkspaceRoot = path.resolve(controlRoot, admission.workspace.path);
|
|
36
|
+
if (workspaceRoot !== expectedWorkspaceRoot) {
|
|
37
|
+
throw new Error(`executePreparedTask: workspace binding mismatch (expected ${expectedWorkspaceRoot}, got ${workspaceRoot})`);
|
|
38
|
+
}
|
|
39
|
+
assertWithinRoot(path.resolve(controlRoot, ".worktrees"), workspaceRoot, "night workspace");
|
|
40
|
+
await access(workspaceRoot);
|
|
41
|
+
if (options.verifyAdmissionHashes !== false) {
|
|
42
|
+
if (!admission.dag?.path || !admission.dag.sha256) {
|
|
43
|
+
throw new Error("executePreparedTask: frozen DAG hash is required");
|
|
44
|
+
}
|
|
45
|
+
await verifyFrozenWorkspaceFile(workspaceRoot, admission.dag.path, admission.dag.sha256, "DAG");
|
|
46
|
+
if (admission.taskSpec) {
|
|
47
|
+
await verifyFrozenWorkspaceFile(workspaceRoot, admission.taskSpec.path, admission.taskSpec.sha256, "TaskSpec");
|
|
48
|
+
}
|
|
49
|
+
if (admission.taskGraph) {
|
|
50
|
+
await verifyFrozenWorkspaceFile(workspaceRoot, admission.taskGraph.path, admission.taskGraph.sha256, "TaskGraph");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const workerRunId = options.workerRunId ?? buildWorkerRunId(admission.taskId, now);
|
|
54
|
+
const gateToken = admission.gateApproval.token;
|
|
55
|
+
const advance = options.executeGate ??
|
|
56
|
+
(async (input) => options.client.run([
|
|
57
|
+
"task",
|
|
58
|
+
"advance",
|
|
59
|
+
input.harnessTaskId,
|
|
60
|
+
"--approve-gate",
|
|
61
|
+
input.gateToken,
|
|
62
|
+
"--json",
|
|
63
|
+
], {
|
|
64
|
+
cwd: input.workspaceRoot,
|
|
65
|
+
artifactName: `night-execute-${input.workerRunId}`,
|
|
66
|
+
expectJson: true,
|
|
67
|
+
}));
|
|
68
|
+
const advanceResult = await advance({
|
|
69
|
+
workspaceRoot,
|
|
70
|
+
harnessTaskId: admission.harnessTaskId,
|
|
71
|
+
gateToken,
|
|
72
|
+
workerRunId,
|
|
73
|
+
});
|
|
74
|
+
const succeeded = isAdvanceSuccess(advanceResult);
|
|
75
|
+
const status = succeeded ? "succeeded" : "failed";
|
|
76
|
+
const dagPath = admission.dag?.path
|
|
77
|
+
? path.isAbsolute(admission.dag.path)
|
|
78
|
+
? admission.dag.path
|
|
79
|
+
: path.join(workspaceRoot, admission.dag.path)
|
|
80
|
+
: path.join(workspaceRoot, ".harness", "tasks", admission.harnessTaskId, "dag.json");
|
|
81
|
+
const runRecordPath = path.join(workspaceRoot, ".harness", "tasks", admission.harnessTaskId, "artifacts", "worker-run-record.json");
|
|
82
|
+
return {
|
|
83
|
+
status,
|
|
84
|
+
workerRunId,
|
|
85
|
+
businessId: admission.taskId,
|
|
86
|
+
harnessTaskId: admission.harnessTaskId,
|
|
87
|
+
runRecordPath,
|
|
88
|
+
dagPath,
|
|
89
|
+
reportDecision: {
|
|
90
|
+
succeeded,
|
|
91
|
+
reason: succeeded
|
|
92
|
+
? "prepared-execute-gate-consumed"
|
|
93
|
+
: "prepared-execute-failed",
|
|
94
|
+
...(advanceResult.exitCode !== null &&
|
|
95
|
+
advanceResult.exitCode !== undefined
|
|
96
|
+
? { runStatus: `exit-${advanceResult.exitCode}` }
|
|
97
|
+
: {}),
|
|
98
|
+
},
|
|
99
|
+
workspaceRoot,
|
|
100
|
+
admissionScheduleId: admission.scheduleId,
|
|
101
|
+
gateTokenConsumed: gateToken,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
async function verifyFrozenWorkspaceFile(workspaceRoot, fileRef, expectedSha256, label) {
|
|
105
|
+
if (path.isAbsolute(fileRef)) {
|
|
106
|
+
throw new Error(`executePreparedTask: ${label} path must be workspace-relative`);
|
|
107
|
+
}
|
|
108
|
+
const absolutePath = path.resolve(workspaceRoot, fileRef);
|
|
109
|
+
assertWithinRoot(workspaceRoot, absolutePath, label);
|
|
110
|
+
try {
|
|
111
|
+
const bytes = await readFile(absolutePath);
|
|
112
|
+
const actual = createHash("sha256").update(bytes).digest("hex");
|
|
113
|
+
const expected = expectedSha256.replace(/^sha256:/, "");
|
|
114
|
+
if (actual !== expected) {
|
|
115
|
+
throw new Error(`executePreparedTask: ${label} hash drift (admission ${expectedSha256}, actual sha256:${actual})`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
if (error instanceof Error && error.message.includes("hash drift")) {
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
throw new Error(`executePreparedTask: cannot verify ${label} at ${absolutePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function assertWithinRoot(root, candidate, label) {
|
|
126
|
+
const relative = path.relative(path.resolve(root), path.resolve(candidate));
|
|
127
|
+
if (relative.length === 0 ||
|
|
128
|
+
relative.startsWith("..") ||
|
|
129
|
+
path.isAbsolute(relative)) {
|
|
130
|
+
throw new Error(`executePreparedTask: ${label} escapes its allowed root`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function isAdvanceSuccess(result) {
|
|
134
|
+
if (result.ok)
|
|
135
|
+
return true;
|
|
136
|
+
// Mirror run-task terminal failure semantics loosely: non-zero with
|
|
137
|
+
// lifecycle terminal payload still counts as completed attempt (failed).
|
|
138
|
+
const root = result.json;
|
|
139
|
+
if (!root || typeof root !== "object")
|
|
140
|
+
return false;
|
|
141
|
+
const envelope = root;
|
|
142
|
+
const payload = envelope.result && typeof envelope.result === "object"
|
|
143
|
+
? envelope.result
|
|
144
|
+
: envelope;
|
|
145
|
+
const lifecycle = typeof payload.lifecycleState === "string"
|
|
146
|
+
? payload.lifecycleState
|
|
147
|
+
: typeof payload.status === "string"
|
|
148
|
+
? payload.status
|
|
149
|
+
: "";
|
|
150
|
+
if (/success|succeeded|done|completed/i.test(lifecycle))
|
|
151
|
+
return true;
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { deriveFailureRoute, deriveFailureRouteFromError, } from "../pool/failure-routing.js";
|
|
2
|
+
import { recordTaskPoolRun, writeTaskPoolState } from "../pool/run-store.js";
|
|
3
|
+
/**
|
|
4
|
+
* Shared Worker attempt used by batch run-ready and Night Scheduler.
|
|
5
|
+
* Owns Task Pool Running/Done/Failed projection + run record; does not select tasks.
|
|
6
|
+
*/
|
|
7
|
+
export async function runSingleTaskAttempt(input) {
|
|
8
|
+
const now = input.ctx.now ?? new Date();
|
|
9
|
+
const { ctx } = input;
|
|
10
|
+
await writeTaskPoolState(ctx.controlRepoRoot, {
|
|
11
|
+
schemaVersion: 2,
|
|
12
|
+
featureId: ctx.featureId,
|
|
13
|
+
taskId: ctx.taskId,
|
|
14
|
+
status: "Running",
|
|
15
|
+
updatedAt: now.toISOString(),
|
|
16
|
+
workerRunId: ctx.workerRunId,
|
|
17
|
+
...(ctx.night?.nightScheduleId
|
|
18
|
+
? { nightScheduleId: ctx.night.nightScheduleId }
|
|
19
|
+
: {}),
|
|
20
|
+
...(ctx.night?.admissionPath
|
|
21
|
+
? { admissionPath: ctx.night.admissionPath }
|
|
22
|
+
: {}),
|
|
23
|
+
...(ctx.night?.nightWorktreePath
|
|
24
|
+
? { nightWorktreePath: ctx.night.nightWorktreePath }
|
|
25
|
+
: {}),
|
|
26
|
+
...(ctx.night?.nightBranch ? { nightBranch: ctx.night.nightBranch } : {}),
|
|
27
|
+
});
|
|
28
|
+
let result;
|
|
29
|
+
try {
|
|
30
|
+
result = await input.execute(ctx);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
34
|
+
const failure = deriveFailureRouteFromError(message, ctx.taskId);
|
|
35
|
+
const run = {
|
|
36
|
+
schemaVersion: 1,
|
|
37
|
+
batchRunId: ctx.batchRunId,
|
|
38
|
+
workerRunId: ctx.workerRunId,
|
|
39
|
+
taskId: ctx.taskId,
|
|
40
|
+
featureId: ctx.featureId,
|
|
41
|
+
status: "run-error",
|
|
42
|
+
recordedAt: new Date().toISOString(),
|
|
43
|
+
error: message,
|
|
44
|
+
failure,
|
|
45
|
+
...(ctx.controllerIdentity
|
|
46
|
+
? { controllerIdentity: ctx.controllerIdentity }
|
|
47
|
+
: {}),
|
|
48
|
+
};
|
|
49
|
+
await recordTaskPoolRun({
|
|
50
|
+
repoRoot: ctx.controlRepoRoot,
|
|
51
|
+
run,
|
|
52
|
+
});
|
|
53
|
+
// Restore night fields after recordTaskPoolRun overwrites state from run.
|
|
54
|
+
await writeTaskPoolState(ctx.controlRepoRoot, {
|
|
55
|
+
schemaVersion: 2,
|
|
56
|
+
featureId: ctx.featureId,
|
|
57
|
+
taskId: ctx.taskId,
|
|
58
|
+
status: "Failed",
|
|
59
|
+
updatedAt: new Date().toISOString(),
|
|
60
|
+
workerRunId: ctx.workerRunId,
|
|
61
|
+
failure,
|
|
62
|
+
...(ctx.night?.nightScheduleId
|
|
63
|
+
? { nightScheduleId: ctx.night.nightScheduleId }
|
|
64
|
+
: {}),
|
|
65
|
+
...(ctx.night?.admissionPath
|
|
66
|
+
? { admissionPath: ctx.night.admissionPath }
|
|
67
|
+
: {}),
|
|
68
|
+
...(ctx.night?.nightWorktreePath
|
|
69
|
+
? { nightWorktreePath: ctx.night.nightWorktreePath }
|
|
70
|
+
: {}),
|
|
71
|
+
...(ctx.night?.nightBranch ? { nightBranch: ctx.night.nightBranch } : {}),
|
|
72
|
+
});
|
|
73
|
+
return {
|
|
74
|
+
status: "run-error",
|
|
75
|
+
workerRunId: ctx.workerRunId,
|
|
76
|
+
run,
|
|
77
|
+
error: message,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const failure = result.outcomeFailure
|
|
81
|
+
? {
|
|
82
|
+
category: result.outcomeFailure.category,
|
|
83
|
+
recommendedFollowUpKind: "manual-review",
|
|
84
|
+
derivedFollowUpTaskId: `${ctx.taskId}-outcome-contract-review`,
|
|
85
|
+
source: "report-decision",
|
|
86
|
+
}
|
|
87
|
+
: deriveFailureRoute(result);
|
|
88
|
+
const run = {
|
|
89
|
+
schemaVersion: 1,
|
|
90
|
+
batchRunId: ctx.batchRunId,
|
|
91
|
+
workerRunId: result.workerRunId,
|
|
92
|
+
taskId: result.businessId,
|
|
93
|
+
featureId: ctx.featureId,
|
|
94
|
+
status: result.status,
|
|
95
|
+
harnessTaskId: result.harnessTaskId,
|
|
96
|
+
runRecordPath: result.runRecordPath,
|
|
97
|
+
dagPath: result.dagPath,
|
|
98
|
+
recordedAt: new Date().toISOString(),
|
|
99
|
+
...(failure ? { failure } : {}),
|
|
100
|
+
...(result.failureArtifacts
|
|
101
|
+
? { failureArtifacts: result.failureArtifacts }
|
|
102
|
+
: {}),
|
|
103
|
+
...(ctx.controllerIdentity
|
|
104
|
+
? { controllerIdentity: ctx.controllerIdentity }
|
|
105
|
+
: {}),
|
|
106
|
+
...(result.workflow ? { workflow: result.workflow } : {}),
|
|
107
|
+
...(result.outcome
|
|
108
|
+
? {
|
|
109
|
+
outcomePath: result.outcome.outcomePath,
|
|
110
|
+
outcomeSha256: result.outcome.outcomeSha256,
|
|
111
|
+
}
|
|
112
|
+
: {}),
|
|
113
|
+
};
|
|
114
|
+
await recordTaskPoolRun({
|
|
115
|
+
repoRoot: ctx.controlRepoRoot,
|
|
116
|
+
run,
|
|
117
|
+
});
|
|
118
|
+
const terminalStatus = result.status === "succeeded" ? "Done" : "Failed";
|
|
119
|
+
const poolState = {
|
|
120
|
+
schemaVersion: 2,
|
|
121
|
+
featureId: ctx.featureId,
|
|
122
|
+
taskId: ctx.taskId,
|
|
123
|
+
status: terminalStatus,
|
|
124
|
+
updatedAt: new Date().toISOString(),
|
|
125
|
+
workerRunId: result.workerRunId,
|
|
126
|
+
lastRunRecordPath: result.runRecordPath,
|
|
127
|
+
...(failure ? { failure } : {}),
|
|
128
|
+
...(ctx.night?.nightScheduleId
|
|
129
|
+
? { nightScheduleId: ctx.night.nightScheduleId }
|
|
130
|
+
: {}),
|
|
131
|
+
...(ctx.night?.admissionPath
|
|
132
|
+
? { admissionPath: ctx.night.admissionPath }
|
|
133
|
+
: {}),
|
|
134
|
+
...(ctx.night?.nightWorktreePath
|
|
135
|
+
? { nightWorktreePath: ctx.night.nightWorktreePath }
|
|
136
|
+
: {}),
|
|
137
|
+
...(ctx.night?.nightBranch ? { nightBranch: ctx.night.nightBranch } : {}),
|
|
138
|
+
};
|
|
139
|
+
await writeTaskPoolState(ctx.controlRepoRoot, poolState);
|
|
140
|
+
return {
|
|
141
|
+
status: result.status,
|
|
142
|
+
workerRunId: result.workerRunId,
|
|
143
|
+
run,
|
|
144
|
+
result,
|
|
145
|
+
poolState,
|
|
146
|
+
};
|
|
147
|
+
}
|