@tea-agent/loop-agent 0.12.0 → 0.13.0-alpha.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/CHANGELOG.md +50 -2
- package/README.md +30 -2
- package/dist/application/dag/generate-task-dag.js +30 -0
- package/dist/cli/command-definitions.js +10 -3
- package/dist/commands/knowledge.js +129 -31
- package/dist/governance/manifest-types.js +3 -0
- package/dist/task/config-types.js +5 -1
- package/dist/worker/cli.js +96 -1
- package/dist/worker/delivery/package.js +3 -3
- package/dist/worker/feature/decision-loader.js +37 -6
- package/dist/worker/feature/next-action.js +10 -2
- package/dist/worker/feature/ready-plan-projection.js +81 -0
- package/dist/worker/feature/reducer.js +2 -1
- package/dist/worker/feature/review.js +19 -2
- package/dist/worker/feature/run.js +27 -2
- package/dist/worker/follow-up/approve.js +5 -2
- package/dist/worker/follow-up/factory.js +1 -1
- package/dist/worker/observability/read-model.js +246 -41
- package/dist/worker/observe/routes.js +158 -15
- package/dist/worker/observe/spec-evidence.js +281 -0
- package/dist/worker/observe/static/api.js +19 -0
- package/dist/worker/observe/static/app.js +2 -2
- package/dist/worker/observe/static/relations.js +17 -12
- package/dist/worker/observe/static/router.js +8 -0
- package/dist/worker/observe/static/styles.css +12 -0
- package/dist/worker/observe/static/views/batch.js +3 -2
- package/dist/worker/observe/static/views/dag-inspector.js +123 -4
- package/dist/worker/observe/static/views/dashboard.js +8 -5
- package/dist/worker/observe/static/views/feature.js +43 -4
- package/dist/worker/observe/static/views/pool.js +5 -2
- package/dist/worker/observe/static/views/run.js +1 -1
- package/dist/worker/observe/static/views/task.js +69 -15
- package/dist/worker/pool/doctor.js +165 -0
- package/dist/worker/pool/migrate-state.js +303 -0
- package/dist/worker/pool/run-store.js +205 -17
- package/dist/worker/pool/types.js +17 -1
- package/dist/worker/pool/validation.js +100 -15
- package/dist/worker/report/morning-report.js +12 -2
- package/dist/worker/runner/run-ready.js +41 -26
- package/dist/worker/task-graph/ready-planner.js +136 -0
- package/dist/workflows/dag/convergence/controller.js +16 -8
- package/dist/workflows/dag/failure-routing.js +12 -1
- package/dist/workflows/dag/init-hybrid.js +837 -8
- package/dist/workflows/dag/types.js +1 -0
- package/docs/README.md +1 -1
- package/docs/agent-dag-recovery-playbook.md +9 -0
- package/docs/architecture/evolution.md +4 -3
- package/docs/architecture/facts-and-state.md +14 -1
- package/docs/architecture/worker-and-feature.md +6 -2
- package/docs/decisions/README.md +3 -0
- package/docs/design/README.md +8 -0
- package/docs/exec-plans/active/README.md +2 -0
- package/docs/exec-plans/completed/README.md +3 -2
- package/docs/feature-workflow.md +80 -2
- package/docs/loop-agent-harness.md +15 -4
- package/docs/progress/README.md +4 -0
- package/docs/reports/README.md +6 -0
- package/docs/templates/backend-test-dag.json +12 -0
- package/docs/templates/knowledge-graph-bootstrap-dag.json +118 -0
- package/docs/templates/knowledge-sync-dag.json +177 -0
- package/docs/templates/knowledge-sync-draft.schema.json +71 -0
- package/docs/verification-matrix.md +2 -1
- package/package.json +8 -2
- package/scripts/kb-bootstrap-init-skeleton.sh +239 -0
- package/scripts/kb-graph-incremental-prepare.mjs +372 -0
- package/scripts/kb-graph-incremental-prepare.sh +5 -0
- package/scripts/kb-graph-materialize.mjs +105 -0
- package/scripts/kb-graph-materialize.sh +4 -0
- package/scripts/kb-graph-promote.mjs +153 -0
- package/scripts/kb-graph-promote.sh +4 -0
- package/scripts/kb-query.mjs +554 -0
- package/scripts/kb-query.sh +5 -0
- package/skills/agent-worker/SKILL.md +3 -1
- package/skills/agent-worker/references/agent-worker-operator.md +18 -1
- package/skills/frontend-design-review/SKILL.md +26 -24
- package/skills/frontend-implementation/SKILL.md +29 -26
- package/skills/frontend-implementation/references/node-contracts.md +50 -19
- package/skills/frontend-review/SKILL.md +1 -1
- package/skills/loop-agent/references/command-reference.md +1 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import YAML from "yaml";
|
|
4
|
+
import { planReadyTasks, } from "../task-graph/ready-planner.js";
|
|
5
|
+
import { taskGraphSpecSchema } from "../task-graph/task-graph-schema.js";
|
|
6
|
+
import { taskSpecSchema } from "../task-spec/schema.js";
|
|
7
|
+
/** Single Feature-side entry to M1 planReadyTasks; never reimplements ordering. */
|
|
8
|
+
export function projectReadyPlan(input) {
|
|
9
|
+
return planReadyTasks(input);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Apply a tighter selectionLimit to an existing planner projection.
|
|
13
|
+
* Eligible order is preserved from planReadyTasks; this does not re-sort.
|
|
14
|
+
*/
|
|
15
|
+
export function applySelectionLimit(plan, selectionLimit) {
|
|
16
|
+
if (!Number.isInteger(selectionLimit) || selectionLimit < 1) {
|
|
17
|
+
throw new Error("selectionLimit must be a positive integer");
|
|
18
|
+
}
|
|
19
|
+
const selected = plan.eligible.slice(0, selectionLimit);
|
|
20
|
+
const deferred = plan.eligible.slice(selectionLimit).map((candidate) => ({
|
|
21
|
+
...candidate,
|
|
22
|
+
reasonCode: "selection-limit",
|
|
23
|
+
selectedAhead: selected.map((item) => item.taskId),
|
|
24
|
+
}));
|
|
25
|
+
return {
|
|
26
|
+
...plan,
|
|
27
|
+
selectionLimit,
|
|
28
|
+
selected,
|
|
29
|
+
deferred,
|
|
30
|
+
summary: {
|
|
31
|
+
...plan.summary,
|
|
32
|
+
selected: selected.length,
|
|
33
|
+
deferred: deferred.length,
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export function selectedTaskIds(plan) {
|
|
38
|
+
return plan.selected.map((item) => item.taskId);
|
|
39
|
+
}
|
|
40
|
+
export function summarizePlanReasons(plan) {
|
|
41
|
+
return {
|
|
42
|
+
selected: plan.selected.map((item) => ({ taskId: item.taskId, priority: item.priority })),
|
|
43
|
+
deferred: plan.deferred.map((item) => ({
|
|
44
|
+
taskId: item.taskId,
|
|
45
|
+
reasonCode: item.reasonCode,
|
|
46
|
+
selectedAhead: item.selectedAhead,
|
|
47
|
+
})),
|
|
48
|
+
blocked: plan.blocked.map((item) => ({
|
|
49
|
+
taskId: item.taskId,
|
|
50
|
+
reasonCode: item.reasonCode,
|
|
51
|
+
reason: item.reason,
|
|
52
|
+
blockedBy: item.blockedBy,
|
|
53
|
+
})),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export async function loadReadyPlanInputs(featureDir) {
|
|
57
|
+
const resolved = path.resolve(featureDir);
|
|
58
|
+
const graphRaw = YAML.parse(await readFile(path.join(resolved, "tasks", "task-graph.yaml"), "utf-8"));
|
|
59
|
+
const graph = taskGraphSpecSchema.parse(graphRaw);
|
|
60
|
+
const taskSpecs = new Map();
|
|
61
|
+
for (const node of graph.nodes) {
|
|
62
|
+
const value = YAML.parse(await readFile(path.join(resolved, "tasks", node.task), "utf-8"));
|
|
63
|
+
taskSpecs.set(node.id, taskSpecSchema.parse(value));
|
|
64
|
+
}
|
|
65
|
+
return { featureId: graph.feature_id, graph, taskSpecs };
|
|
66
|
+
}
|
|
67
|
+
export async function projectReadyPlanForFeature(input) {
|
|
68
|
+
try {
|
|
69
|
+
const loaded = await loadReadyPlanInputs(input.featureDir);
|
|
70
|
+
return projectReadyPlan({
|
|
71
|
+
featureId: input.featureId || loaded.featureId,
|
|
72
|
+
graph: loaded.graph,
|
|
73
|
+
taskSpecs: loaded.taskSpecs,
|
|
74
|
+
states: input.states,
|
|
75
|
+
selectionLimit: input.selectionLimit,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -10,7 +10,7 @@ const STATUS_LABELS = {
|
|
|
10
10
|
};
|
|
11
11
|
export function reduceFeature(input) {
|
|
12
12
|
const status = deriveStatus(input);
|
|
13
|
-
const readyTasks = input.tasks.filter((task) => isReady(task.taskId, input));
|
|
13
|
+
const readyTasks = input.planning?.eligible ?? input.tasks.filter((task) => isReady(task.taskId, input));
|
|
14
14
|
const blockingItems = [];
|
|
15
15
|
const resolvedFailures = new Set(input.resolvedFailureTaskIds ?? []);
|
|
16
16
|
for (const error of input.validationErrors) {
|
|
@@ -88,6 +88,7 @@ export function reduceFeature(input) {
|
|
|
88
88
|
closeout: input.closeout?.path ?? null,
|
|
89
89
|
},
|
|
90
90
|
projectionWarnings: [...input.projectionWarnings],
|
|
91
|
+
...(input.planning ? { planning: input.planning } : {}),
|
|
91
92
|
};
|
|
92
93
|
}
|
|
93
94
|
function deriveStatus(input) {
|
|
@@ -3,12 +3,14 @@ import { access, readFile, realpath } from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import YAML from "yaml";
|
|
5
5
|
import { z } from "zod";
|
|
6
|
-
import { getRunsJsonlPath, getTaskPoolRoot,
|
|
6
|
+
import { getRunsJsonlPath, getTaskPoolRoot, readFeatureTaskPoolStates, readJsonlFile } from "../pool/run-store.js";
|
|
7
7
|
import { acceptanceSpecSchema } from "../task-graph/acceptance-schema.js";
|
|
8
8
|
import { taskGraphSpecSchema } from "../task-graph/task-graph-schema.js";
|
|
9
|
+
import { taskSpecSchema } from "../task-spec/schema.js";
|
|
9
10
|
import { validateFeatureTaskGraph } from "../task-graph/validate.js";
|
|
10
11
|
import { followUpActionCardSchema, followUpApprovalSchema, followUpDraftSchema, followUpEvidenceSchema } from "../follow-up/schema.js";
|
|
11
12
|
import { readFollowUpIndex, resolveRepoFile, sha256File as sha256FollowUpFile } from "../follow-up/store.js";
|
|
13
|
+
import { projectReadyPlan } from "./ready-plan-projection.js";
|
|
12
14
|
import { reduceFeature } from "./reducer.js";
|
|
13
15
|
export async function reviewFeature(input) {
|
|
14
16
|
const projection = await loadFeatureProjection(input);
|
|
@@ -67,7 +69,7 @@ async function loadFeatureProjection(input) {
|
|
|
67
69
|
let states = {};
|
|
68
70
|
let runs = [];
|
|
69
71
|
try {
|
|
70
|
-
states = await
|
|
72
|
+
states = await readFeatureTaskPoolStates(repoRoot, validation.featureId);
|
|
71
73
|
runs = await readJsonlFile(getRunsJsonlPath(repoRoot));
|
|
72
74
|
}
|
|
73
75
|
catch (error) {
|
|
@@ -86,6 +88,20 @@ async function loadFeatureProjection(input) {
|
|
|
86
88
|
return valid;
|
|
87
89
|
});
|
|
88
90
|
const graphNodes = graph.success ? graph.data.nodes : [];
|
|
91
|
+
let planning;
|
|
92
|
+
if (graph.success) {
|
|
93
|
+
try {
|
|
94
|
+
const taskSpecs = new Map();
|
|
95
|
+
for (const node of graph.data.nodes) {
|
|
96
|
+
const value = YAML.parse(await readFile(path.join(featureDir, "tasks", node.task), "utf-8"));
|
|
97
|
+
taskSpecs.set(node.id, taskSpecSchema.parse(value));
|
|
98
|
+
}
|
|
99
|
+
planning = projectReadyPlan({ featureId: validation.featureId, graph: graph.data, taskSpecs, states, selectionLimit: Math.max(graph.data.nodes.length, 1) });
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
projectionWarnings.push(`Ready Planner projection is unavailable: ${message(error)}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
89
105
|
let pendingFollowUps = [];
|
|
90
106
|
let actionCards = [];
|
|
91
107
|
let resolvedFailureTaskIds = [];
|
|
@@ -292,6 +308,7 @@ async function loadFeatureProjection(input) {
|
|
|
292
308
|
...((await exists(morningReportPath)) ? { morningReport: morningReportPath } : {}),
|
|
293
309
|
...((await exists(observeSnapshotPath)) ? { observeSnapshot: observeSnapshotPath } : {}),
|
|
294
310
|
projectionWarnings,
|
|
311
|
+
...(planning ? { planning } : {}),
|
|
295
312
|
};
|
|
296
313
|
}
|
|
297
314
|
async function exists(filePath) {
|
|
@@ -5,11 +5,12 @@ import { buildGlobalSnapshot } from "../observability/read-model.js";
|
|
|
5
5
|
import { createSnapshotStore } from "../observability/snapshot-store.js";
|
|
6
6
|
import { preflightTargetRepo } from "../preflight.js";
|
|
7
7
|
import { noopProgressReporter } from "../progress-reporter.js";
|
|
8
|
-
import { getTaskPoolRoot } from "../pool/run-store.js";
|
|
8
|
+
import { getTaskPoolRoot, listLegacyStateFiles } from "../pool/run-store.js";
|
|
9
9
|
import { writeMorningReport } from "../report/morning-report.js";
|
|
10
10
|
import { buildBatchRunId, runReadyTasks } from "../runner/run-ready.js";
|
|
11
11
|
import { validateFeatureTaskGraph } from "../task-graph/validate.js";
|
|
12
12
|
import { reviewFeature } from "./review.js";
|
|
13
|
+
import { applySelectionLimit, selectedTaskIds } from "./ready-plan-projection.js";
|
|
13
14
|
import { finalizeGitTask, startGitTransaction } from "../delivery/git-transaction.js";
|
|
14
15
|
export async function runFeature(options) {
|
|
15
16
|
const featureDir = path.resolve(options.featureDir);
|
|
@@ -125,7 +126,13 @@ export async function runFeature(options) {
|
|
|
125
126
|
controllerIdentity });
|
|
126
127
|
}
|
|
127
128
|
before = beforeStep.value;
|
|
128
|
-
const
|
|
129
|
+
const fullReadyPlan = before.planning;
|
|
130
|
+
const readyPlan = fullReadyPlan
|
|
131
|
+
? applySelectionLimit(fullReadyPlan, limitApplied)
|
|
132
|
+
: undefined;
|
|
133
|
+
const readyTasks = readyPlan
|
|
134
|
+
? selectedTaskIds(readyPlan)
|
|
135
|
+
: before.tasks.filter((task) => task.status === "Ready").map((task) => task.taskId);
|
|
129
136
|
if (options.dryRun) {
|
|
130
137
|
steps.push({ ...planned("run-ready", readyTasks.length > 0 ? `execute at most ${limitApplied} Ready task` : "no Ready task"), artifacts: [expectedArtifacts.batchRun] }, { ...planned("morning-report", "refresh morning report"), artifacts: [expectedArtifacts.morningReport] }, { ...planned("observe-snapshot", "refresh Observe snapshot"), artifacts: [expectedArtifacts.observeSnapshot] }, planned("review-after", "derive final Feature review"));
|
|
131
138
|
return {
|
|
@@ -138,6 +145,7 @@ export async function runFeature(options) {
|
|
|
138
145
|
statusAfter: before.status,
|
|
139
146
|
steps,
|
|
140
147
|
readyTasks,
|
|
148
|
+
...(readyPlan ? { readyPlan } : {}),
|
|
141
149
|
executedTasks: [],
|
|
142
150
|
artifacts: { batchRun: null, morningReport: null, observeSnapshot: null, ...(checkpointMode ? { gitTransaction: expectedArtifacts.gitTransaction ?? null } : {}) },
|
|
143
151
|
expectedArtifacts,
|
|
@@ -151,6 +159,22 @@ export async function runFeature(options) {
|
|
|
151
159
|
};
|
|
152
160
|
}
|
|
153
161
|
if (readyTasks.length > 0 && limitApplied > 0) {
|
|
162
|
+
const legacyStates = await listLegacyStateFiles(repoRoot);
|
|
163
|
+
if (legacyStates.length > 0) {
|
|
164
|
+
return failedResult({
|
|
165
|
+
featureId,
|
|
166
|
+
mode: "executed",
|
|
167
|
+
steps,
|
|
168
|
+
limitApplied,
|
|
169
|
+
code: "task-pool-state-migration-required",
|
|
170
|
+
message: `cannot execute Ready tasks while legacy Task Pool states require migration: ${legacyStates.map((item) => item.taskId).join(", ")}`,
|
|
171
|
+
evidence: legacyStates.map((item) => item.path),
|
|
172
|
+
expectedArtifacts,
|
|
173
|
+
controllerIdentity,
|
|
174
|
+
before,
|
|
175
|
+
readyTasks,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
154
178
|
progress.batch(`feature ${featureId}: executing ${Math.min(readyTasks.length, limitApplied)} of ${readyTasks.length} Ready task(s)`);
|
|
155
179
|
const runStep = await timedStep(steps, "run-ready", async () => dependencies.runReady({
|
|
156
180
|
repoRoot,
|
|
@@ -226,6 +250,7 @@ export async function runFeature(options) {
|
|
|
226
250
|
statusAfter: after.value.status,
|
|
227
251
|
steps,
|
|
228
252
|
readyTasks,
|
|
253
|
+
...(readyPlan ? { readyPlan } : {}),
|
|
229
254
|
executedTasks: batch?.tasks ?? [],
|
|
230
255
|
artifacts: { batchRun: batch?.batchRunPath ?? null, morningReport: report.value, observeSnapshot: snapshot.value, ...(gitTransaction ? { gitTransaction: gitTransaction.recordPath } : {}) },
|
|
231
256
|
expectedArtifacts,
|
|
@@ -99,6 +99,8 @@ async function executeApproval(input) {
|
|
|
99
99
|
}
|
|
100
100
|
await input.faultInjector?.("before-state-write");
|
|
101
101
|
await writeTaskPoolState(input.repoRoot, {
|
|
102
|
+
schemaVersion: 2,
|
|
103
|
+
featureId: input.featureId,
|
|
102
104
|
taskId: loaded.draft.proposed_task_id,
|
|
103
105
|
status: "Ready",
|
|
104
106
|
updatedAt: (input.now ?? new Date()).toISOString(),
|
|
@@ -201,7 +203,7 @@ async function loadApprovalInputs(input) {
|
|
|
201
203
|
entry, draft, draftPath, evidencePath, approvalPath, existingApproval,
|
|
202
204
|
taskSpecPath: path.join(input.featureDir, "tasks", `${draft.proposed_task_id}.yaml`),
|
|
203
205
|
graphPath: path.join(input.featureDir, "tasks", "task-graph.yaml"),
|
|
204
|
-
statePath: getTaskStatePath(input.repoRoot, draft.proposed_task_id),
|
|
206
|
+
statePath: getTaskStatePath(input.repoRoot, { featureId: input.featureId, taskId: draft.proposed_task_id }),
|
|
205
207
|
featureId: input.featureId,
|
|
206
208
|
featureDir: input.featureDir,
|
|
207
209
|
repoRoot: input.repoRoot,
|
|
@@ -237,8 +239,9 @@ async function assertApprovalPreconditions(loaded) {
|
|
|
237
239
|
const graph = taskGraphSpecSchema.parse(YAML.parse(await readFile(loaded.graphPath, "utf-8")));
|
|
238
240
|
if (graph.nodes.some((node) => node.id === loaded.draft.proposed_task_id))
|
|
239
241
|
throw new Error(`proposed graph node already exists: ${loaded.draft.proposed_task_id}`);
|
|
240
|
-
if (await readTaskPoolState(loaded.repoRoot, loaded.draft.proposed_task_id))
|
|
242
|
+
if (await readTaskPoolState(loaded.repoRoot, { featureId: loaded.featureId, taskId: loaded.draft.proposed_task_id })) {
|
|
241
243
|
throw new Error(`Task Pool state already exists: ${loaded.draft.proposed_task_id}`);
|
|
244
|
+
}
|
|
242
245
|
}
|
|
243
246
|
function resultFromApproval(approval, repoRoot, approvalPath, idempotent) {
|
|
244
247
|
return {
|
|
@@ -99,7 +99,7 @@ export async function draftFollowUpDecision(input, productBugOnly = false) {
|
|
|
99
99
|
generated_from: { feature_id: run.featureId, task_id: input.taskId, worker_run_id: input.workerRunId, failure_category: category },
|
|
100
100
|
action: policy.action,
|
|
101
101
|
label: policy.label,
|
|
102
|
-
...(policy.recommendedCommand === "task-retry" ? { command: `agent-worker task retry ${JSON.stringify(input.taskId)} --repo ${JSON.stringify(repoRoot)}` } : {}),
|
|
102
|
+
...(policy.recommendedCommand === "task-retry" ? { command: `agent-worker task retry ${JSON.stringify(input.taskId)} --feature-id ${JSON.stringify(run.featureId)} --repo ${JSON.stringify(repoRoot)}` } : {}),
|
|
103
103
|
evidence_refs: evidenceRefs,
|
|
104
104
|
dedupe_key: dedupeKey,
|
|
105
105
|
created_at: (input.now ?? new Date()).toISOString(),
|