@tea-agent/loop-agent 0.28.2-beta.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.
Files changed (67) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +25 -0
  3. package/README.md +11 -1
  4. package/dist/cli/command-definitions.js +2 -1
  5. package/dist/commands/client-recovery.js +111 -8
  6. package/dist/commands/dag-init-hybrid.js +1 -1
  7. package/dist/commands/init-upgrade.js +2479 -0
  8. package/dist/commands/init.js +120 -9
  9. package/dist/governance/manifest-types.js +65 -0
  10. package/dist/shared/operator/capabilities.js +350 -2
  11. package/dist/task/worktree.js +256 -39
  12. package/dist/worker/cli.js +22 -12
  13. package/dist/worker/console/chat/workspace-landing.js +16 -6
  14. package/dist/worker/console/observe-health-match.js +2 -0
  15. package/dist/worker/console/observe-link.js +4 -0
  16. package/dist/worker/console/operator-actions.js +183 -4
  17. package/dist/worker/console/operator-selection.js +13 -0
  18. package/dist/worker/console/static/assets/index-BfRgtLF4.js +29 -0
  19. package/dist/worker/console/static/index.html +1 -1
  20. package/dist/worker/observe/health.js +1 -0
  21. package/dist/worker/observe/night-jobs.js +104 -0
  22. package/dist/worker/observe/routes.js +48 -0
  23. package/dist/worker/observe/static/app.js +3 -0
  24. package/dist/worker/observe/static/constants.js +1 -0
  25. package/dist/worker/observe/static/index.html +47 -0
  26. package/dist/worker/observe/static/router.js +10 -0
  27. package/dist/worker/observe/static/shell-chrome.js +1 -0
  28. package/dist/worker/observe/static/views/night.js +201 -0
  29. package/dist/worker/report/morning-report.js +56 -16
  30. package/dist/worker/run-task/execute-prepared-task.js +153 -0
  31. package/dist/worker/runner/single-task-attempt.js +147 -0
  32. package/dist/worker/scheduler/admission.js +536 -0
  33. package/dist/worker/scheduler/auto-followup.js +99 -0
  34. package/dist/worker/scheduler/cli.js +539 -0
  35. package/dist/worker/scheduler/dispatcher.js +503 -0
  36. package/dist/worker/scheduler/doctor.js +346 -0
  37. package/dist/worker/scheduler/evidence.js +170 -0
  38. package/dist/worker/scheduler/git-base.js +52 -0
  39. package/dist/worker/scheduler/index.js +23 -0
  40. package/dist/worker/scheduler/lease.js +114 -0
  41. package/dist/worker/scheduler/lifecycle.js +348 -0
  42. package/dist/worker/scheduler/lock.js +80 -0
  43. package/dist/worker/scheduler/morning-window.js +161 -0
  44. package/dist/worker/scheduler/night-git-finalizer.js +88 -0
  45. package/dist/worker/scheduler/night-harvest.js +421 -0
  46. package/dist/worker/scheduler/paths.js +84 -0
  47. package/dist/worker/scheduler/prepared-attempt-recovery.js +471 -0
  48. package/dist/worker/scheduler/recovery.js +277 -0
  49. package/dist/worker/scheduler/reservation.js +146 -0
  50. package/dist/worker/scheduler/retry.js +199 -0
  51. package/dist/worker/scheduler/scheduler-loop.js +272 -0
  52. package/dist/worker/scheduler/store.js +275 -0
  53. package/dist/worker/scheduler/traceability.js +54 -0
  54. package/dist/worker/scheduler/trigger.js +258 -0
  55. package/dist/worker/scheduler/types.js +369 -0
  56. package/dist/worker/scheduler/workspace-adapter.js +91 -0
  57. package/dist/workflows/dag/frontend-implementation-contract.js +2 -102
  58. package/docs/architecture/runtime-boundaries.md +9 -0
  59. package/docs/init-surface.manifest.json +9 -2
  60. package/docs/templates/harness.schema.json +107 -0
  61. package/docs/templates/init-managed-agents.md +18 -8
  62. package/harness.json +22 -0
  63. package/package.json +1 -1
  64. package/skills/loop-agent/SKILL.md +28 -36
  65. package/skills/loop-agent/references/command-reference.md +40 -16
  66. package/skills/loop-agent/references/hybrid-dag.md +1 -1
  67. 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
+ }