@tea-agent/loop-agent 0.31.1 → 0.32.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 +1 -1
- package/CHANGELOG.md +32 -0
- package/README.md +1 -1
- package/dist/shared/operator/capabilities.js +22 -1
- package/dist/shared/operator/command-lifecycle.js +94 -0
- package/dist/shared/operator/index.js +1 -0
- package/dist/worker/cli.js +10 -24
- package/dist/worker/console/doctor.js +11 -4
- package/dist/worker/console/observe-health-match.js +18 -15
- package/dist/worker/console/operator-actions.js +2 -1
- package/dist/worker/feature/acceptance-policy.js +227 -0
- package/dist/worker/feature/decision-loader.js +1 -1
- package/dist/worker/feature/next-action.js +56 -6
- package/dist/worker/feature/profile-schema.js +3 -0
- package/dist/worker/feature/reducer.js +1 -0
- package/dist/worker/feature/review.js +72 -8
- package/dist/worker/feature/scaffold.js +14 -0
- package/dist/worker/loop-agent/controller-protocol.js +143 -0
- package/dist/worker/materialize/harness-task-lifecycle-probe.js +126 -0
- package/dist/worker/materialize/harness-task-lineage.js +220 -0
- package/dist/worker/materialize/harness-task-materializer.js +350 -80
- package/dist/worker/observability/progress-composite.js +1 -0
- package/dist/worker/observability/read-model.js +66 -19
- package/dist/worker/pool/attempt-identity.js +41 -0
- package/dist/worker/pool/attempt-lease.js +184 -0
- package/dist/worker/pool/attempt-transition.js +210 -0
- package/dist/worker/pool/begin-attempt-with-lease.js +26 -0
- package/dist/worker/pool/begin-attempt.js +35 -0
- package/dist/worker/pool/failure-routing.js +49 -0
- package/dist/worker/pool/recovery-decision.js +163 -0
- package/dist/worker/pool/run-owner-store.js +126 -0
- package/dist/worker/pool/run-store.js +32 -46
- package/dist/worker/pool/runtime-reconcile-inventory.js +127 -0
- package/dist/worker/pool/state-projection.js +57 -0
- package/dist/worker/run-task/run-task.js +31 -4
- package/dist/worker/runner/run-ready.js +64 -14
- package/dist/worker/runner/single-task-attempt.js +42 -13
- package/dist/worker/task-graph/acceptance-schema.js +3 -0
- package/docs/README.md +3 -1
- package/docs/architecture/evolution.md +1 -1
- package/docs/operations/README.md +1 -0
- package/docs/templates/README.md +1 -0
- package/docs/templates/agent-worker-production-readiness-checklist.md +45 -0
- package/docs/templates/evaluation/agents-map-slim-v1.md +1 -1
- package/docs/templates/evaluation/agents-map-verbose-v0.md +1 -1
- package/docs/templates/init-managed-agents.md +1 -1
- package/docs/templates/product-line/scaffold-samples/backend-only/acceptance.yaml +3 -0
- package/docs/templates/product-line/scaffold-samples/backend-only/feature.yaml +11 -0
- package/docs/templates/product-line/scaffold-samples/fe-with-api/acceptance.yaml +3 -0
- package/docs/templates/product-line/scaffold-samples/fe-with-api/feature.yaml +11 -0
- package/docs/templates/product-line/scaffold-samples/frontend-only/acceptance.yaml +3 -0
- package/docs/templates/product-line/scaffold-samples/frontend-only/feature.yaml +11 -0
- package/package.json +1 -1
- package/skills/agent-worker/SKILL.md +1 -1
- package/skills/agent-worker/references/agent-worker-operator.md +1 -1
- package/skills/loop-agent/references/command-reference.md +2 -2
- package/skills/loop-agent/references/harness-policy.md +1 -1
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { sha256OfCanonicalJson } from "../../task/contract/hash.js";
|
|
5
|
+
import { resolveLoopAgentProfile } from "../profile-mapping.js";
|
|
6
|
+
import { mapRiskLevelToComplexity } from "../task-spec/complexity-mapping.js";
|
|
7
|
+
import { resolveWorkflow, } from "../task-spec/workflow-routing.js";
|
|
8
|
+
export const HARNESS_TASK_LINEAGE_MATERIALIZATION_VERSION = 1;
|
|
9
|
+
export const MAX_HARNESS_TASK_ID_LENGTH = 96;
|
|
10
|
+
export function slugifyIdentity(value) {
|
|
11
|
+
return value
|
|
12
|
+
.normalize("NFKD")
|
|
13
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
14
|
+
.toLowerCase()
|
|
15
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
16
|
+
.replace(/^-+|-+$/g, "")
|
|
17
|
+
.replace(/-{2,}/g, "-");
|
|
18
|
+
}
|
|
19
|
+
export function buildHarnessTaskLineage(taskSpec) {
|
|
20
|
+
const lineageId = slugifyIdentity(`${taskSpec.feature_id}-${taskSpec.id}`);
|
|
21
|
+
return {
|
|
22
|
+
schemaVersion: 1,
|
|
23
|
+
featureId: taskSpec.feature_id,
|
|
24
|
+
taskId: taskSpec.id,
|
|
25
|
+
lineageId,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export function isLegacyReadableHarnessTaskId(taskId) {
|
|
29
|
+
return /^\d{4}-\d{2}-\d{2}-[a-z0-9]+(-[a-z0-9]+)*$/.test(taskId);
|
|
30
|
+
}
|
|
31
|
+
export function buildHarnessTaskId(lineageId, revisionId) {
|
|
32
|
+
const revisionShort = revisionId.slice(0, 8);
|
|
33
|
+
let base = `${lineageId}--${revisionShort}`;
|
|
34
|
+
if (base.length <= MAX_HARNESS_TASK_ID_LENGTH) {
|
|
35
|
+
return base;
|
|
36
|
+
}
|
|
37
|
+
const hash = createHash("sha256")
|
|
38
|
+
.update(`${lineageId}:${revisionId}`)
|
|
39
|
+
.digest("hex")
|
|
40
|
+
.slice(0, 8);
|
|
41
|
+
const prefixBudget = MAX_HARNESS_TASK_ID_LENGTH - hash.length - 2;
|
|
42
|
+
const prefix = lineageId.slice(0, Math.max(8, prefixBudget)).replace(/-+$/g, "");
|
|
43
|
+
base = `${prefix}--${hash}`;
|
|
44
|
+
if (base.length > MAX_HARNESS_TASK_ID_LENGTH) {
|
|
45
|
+
throw new Error(`harness task id exceeds bounded length (${base.length} > ${MAX_HARNESS_TASK_ID_LENGTH})`);
|
|
46
|
+
}
|
|
47
|
+
return base;
|
|
48
|
+
}
|
|
49
|
+
/** Legacy date+title slug ids remain readable; new ids never depend on title. */
|
|
50
|
+
export function buildLegacyReadableHarnessTaskId(taskSpec, now) {
|
|
51
|
+
const date = now.toISOString().slice(0, 10);
|
|
52
|
+
const slug = slugifyIdentity(`${taskSpec.id}-${taskSpec.title}`);
|
|
53
|
+
return `${date}-${slug}`;
|
|
54
|
+
}
|
|
55
|
+
export async function computeSourceRefHashes(input) {
|
|
56
|
+
const taskSpecDir = path.dirname(path.resolve(input.taskSpecPath));
|
|
57
|
+
const hashes = {};
|
|
58
|
+
for (const [key, relativePath] of Object.entries(input.taskSpec.source_docs)) {
|
|
59
|
+
if (!relativePath)
|
|
60
|
+
continue;
|
|
61
|
+
const sourcePath = path.resolve(taskSpecDir, relativePath);
|
|
62
|
+
const content = await readFile(sourcePath);
|
|
63
|
+
hashes[key] = createHash("sha256").update(content).digest("hex");
|
|
64
|
+
}
|
|
65
|
+
return hashes;
|
|
66
|
+
}
|
|
67
|
+
export function computeRevisionDigests(input) {
|
|
68
|
+
const sourcePayload = {
|
|
69
|
+
materializationVersion: HARNESS_TASK_LINEAGE_MATERIALIZATION_VERSION,
|
|
70
|
+
source_docs: input.taskSpec.source_docs,
|
|
71
|
+
source_ref_hashes: input.sourceRefHashes,
|
|
72
|
+
};
|
|
73
|
+
const sourceDigest = sha256OfCanonicalJson(sourcePayload);
|
|
74
|
+
const contractPayload = {
|
|
75
|
+
materializationVersion: HARNESS_TASK_LINEAGE_MATERIALIZATION_VERSION,
|
|
76
|
+
id: input.taskSpec.id,
|
|
77
|
+
feature_id: input.taskSpec.feature_id,
|
|
78
|
+
repo: input.taskSpec.repo,
|
|
79
|
+
type: input.taskSpec.type,
|
|
80
|
+
priority: input.taskSpec.priority,
|
|
81
|
+
risk_level: input.taskSpec.risk_level,
|
|
82
|
+
capabilities: input.taskSpec.capabilities ?? [],
|
|
83
|
+
depends_on: input.taskSpec.depends_on,
|
|
84
|
+
acceptance_refs: input.taskSpec.acceptance_refs,
|
|
85
|
+
scope: input.taskSpec.scope,
|
|
86
|
+
constraints: input.taskSpec.constraints,
|
|
87
|
+
verify: input.taskSpec.verify,
|
|
88
|
+
worker: input.taskSpec.worker,
|
|
89
|
+
execution: input.taskSpec.execution,
|
|
90
|
+
loop_agent: input.taskSpec.loop_agent,
|
|
91
|
+
outputs: input.taskSpec.outputs,
|
|
92
|
+
failure_policy: input.taskSpec.failure_policy,
|
|
93
|
+
workflow: input.workflow,
|
|
94
|
+
taskKind: input.taskKind,
|
|
95
|
+
loopAgentProfile: input.loopAgentProfile,
|
|
96
|
+
allowedPaths: [...input.allowedPaths].sort(),
|
|
97
|
+
complexity: mapRiskLevelToComplexity(input.taskSpec.risk_level),
|
|
98
|
+
};
|
|
99
|
+
const contractDigest = sha256OfCanonicalJson(contractPayload);
|
|
100
|
+
const revisionId = sha256OfCanonicalJson({
|
|
101
|
+
sourceDigest,
|
|
102
|
+
contractDigest,
|
|
103
|
+
});
|
|
104
|
+
return { sourceDigest, contractDigest, revisionId };
|
|
105
|
+
}
|
|
106
|
+
export async function resolveRevisionDigestsForTaskSpec(input) {
|
|
107
|
+
const resolvedWorkflow = resolveWorkflow(input.taskSpec);
|
|
108
|
+
const profileMapping = resolveLoopAgentProfile(input.taskSpec);
|
|
109
|
+
const allowedPaths = expandAllowedPathsForWorkflow(resolvedWorkflow.workflow, input.taskSpec.constraints.allowed_paths);
|
|
110
|
+
const sourceRefHashes = await computeSourceRefHashes({
|
|
111
|
+
taskSpec: input.taskSpec,
|
|
112
|
+
taskSpecPath: input.taskSpecPath,
|
|
113
|
+
});
|
|
114
|
+
const digests = computeRevisionDigests({
|
|
115
|
+
taskSpec: input.taskSpec,
|
|
116
|
+
workflow: resolvedWorkflow.workflow,
|
|
117
|
+
taskKind: resolvedWorkflow.taskKind,
|
|
118
|
+
loopAgentProfile: profileMapping.loopAgentProfile,
|
|
119
|
+
allowedPaths,
|
|
120
|
+
sourceRefHashes,
|
|
121
|
+
});
|
|
122
|
+
return {
|
|
123
|
+
lineage: buildHarnessTaskLineage(input.taskSpec),
|
|
124
|
+
digests,
|
|
125
|
+
workflow: resolvedWorkflow.workflow,
|
|
126
|
+
taskKind: resolvedWorkflow.taskKind,
|
|
127
|
+
loopAgentProfile: profileMapping.loopAgentProfile,
|
|
128
|
+
allowedPaths,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/** Union TaskSpec allowed_paths with workflow-owned write roots. */
|
|
132
|
+
export function expandAllowedPathsForWorkflow(workflow, allowedPaths) {
|
|
133
|
+
const extras = workflow === "backend-test"
|
|
134
|
+
? ["testcase/**", "docs/test-reports/**"]
|
|
135
|
+
: workflow === "frontend-test"
|
|
136
|
+
? ["testcase/frontend/**", "docs/test-reports/**"]
|
|
137
|
+
: [];
|
|
138
|
+
return Array.from(new Set([...allowedPaths, ...extras]));
|
|
139
|
+
}
|
|
140
|
+
export function parseLineageRevisionManifest(value) {
|
|
141
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
142
|
+
return undefined;
|
|
143
|
+
const record = value;
|
|
144
|
+
if (record.schemaVersion !== 1)
|
|
145
|
+
return undefined;
|
|
146
|
+
const lineage = record.lineage;
|
|
147
|
+
const revision = record.revision;
|
|
148
|
+
if (!lineage || typeof lineage !== "object" || !revision || typeof revision !== "object") {
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
const lineageRecord = lineage;
|
|
152
|
+
const revisionRecord = revision;
|
|
153
|
+
if (lineageRecord.schemaVersion !== 1 ||
|
|
154
|
+
revisionRecord.schemaVersion !== 1 ||
|
|
155
|
+
typeof record.harnessTaskId !== "string" ||
|
|
156
|
+
typeof lineageRecord.lineageId !== "string" ||
|
|
157
|
+
typeof revisionRecord.revisionId !== "string" ||
|
|
158
|
+
typeof revisionRecord.sourceDigest !== "string" ||
|
|
159
|
+
typeof revisionRecord.contractDigest !== "string" ||
|
|
160
|
+
typeof revisionRecord.createdAt !== "string") {
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
schemaVersion: 1,
|
|
165
|
+
lineage: {
|
|
166
|
+
schemaVersion: 1,
|
|
167
|
+
featureId: String(lineageRecord.featureId),
|
|
168
|
+
taskId: String(lineageRecord.taskId),
|
|
169
|
+
lineageId: String(lineageRecord.lineageId),
|
|
170
|
+
},
|
|
171
|
+
revision: {
|
|
172
|
+
schemaVersion: 1,
|
|
173
|
+
lineageId: String(revisionRecord.lineageId),
|
|
174
|
+
revisionId: String(revisionRecord.revisionId),
|
|
175
|
+
sourceDigest: String(revisionRecord.sourceDigest),
|
|
176
|
+
contractDigest: String(revisionRecord.contractDigest),
|
|
177
|
+
createdAt: String(revisionRecord.createdAt),
|
|
178
|
+
},
|
|
179
|
+
harnessTaskId: record.harnessTaskId,
|
|
180
|
+
...(typeof record.previousRevisionId === "string"
|
|
181
|
+
? { previousRevisionId: record.previousRevisionId }
|
|
182
|
+
: {}),
|
|
183
|
+
...(typeof record.legacyReadableId === "string"
|
|
184
|
+
? { legacyReadableId: record.legacyReadableId }
|
|
185
|
+
: {}),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
export function parseLineageIndex(value) {
|
|
189
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
190
|
+
return undefined;
|
|
191
|
+
const record = value;
|
|
192
|
+
if (record.schemaVersion !== 1 ||
|
|
193
|
+
typeof record.lineageId !== "string" ||
|
|
194
|
+
typeof record.featureId !== "string" ||
|
|
195
|
+
typeof record.taskId !== "string" ||
|
|
196
|
+
typeof record.currentRevisionId !== "string" ||
|
|
197
|
+
!Array.isArray(record.revisions)) {
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
200
|
+
const revisions = record.revisions
|
|
201
|
+
.filter((entry) => entry && typeof entry === "object")
|
|
202
|
+
.map((entry) => {
|
|
203
|
+
const revision = entry;
|
|
204
|
+
return {
|
|
205
|
+
revisionId: String(revision.revisionId),
|
|
206
|
+
harnessTaskId: String(revision.harnessTaskId),
|
|
207
|
+
sourceDigest: String(revision.sourceDigest),
|
|
208
|
+
contractDigest: String(revision.contractDigest),
|
|
209
|
+
createdAt: String(revision.createdAt),
|
|
210
|
+
};
|
|
211
|
+
});
|
|
212
|
+
return {
|
|
213
|
+
schemaVersion: 1,
|
|
214
|
+
lineageId: record.lineageId,
|
|
215
|
+
featureId: record.featureId,
|
|
216
|
+
taskId: record.taskId,
|
|
217
|
+
currentRevisionId: record.currentRevisionId,
|
|
218
|
+
revisions,
|
|
219
|
+
};
|
|
220
|
+
}
|