@tea-agent/loop-agent 0.26.5-beta.2 → 0.27.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 +2 -2
- package/CHANGELOG.md +33 -0
- package/README.md +13 -23
- package/dist/application/dag/args.js +7 -7
- package/dist/application/dag/generate-task-dag.js +2 -2
- package/dist/application/dag/run-dag.js +3 -4
- package/dist/application/dag/validate-dag.js +1 -1
- package/dist/application/task-lifecycle/advance.js +845 -0
- package/dist/application/task-lifecycle/gates.js +80 -0
- package/dist/application/task-lifecycle/index.js +7 -0
- package/dist/application/task-lifecycle/observe.js +395 -0
- package/dist/application/task-lifecycle/plan-transitions.js +224 -0
- package/dist/application/task-lifecycle/recommendations.js +180 -0
- package/dist/application/task-lifecycle/record.js +73 -0
- package/dist/application/task-lifecycle/types.js +1 -0
- package/dist/cli/command-definitions.js +14 -111
- package/dist/cli/help.js +6 -7
- package/dist/cli/program.js +26 -90
- package/dist/cli/update/policy.js +0 -1
- package/dist/commands/dag-final-verification.js +1 -1
- package/dist/commands/dag-init-hybrid.js +1 -1
- package/dist/commands/delegate.js +2 -2
- package/dist/commands/init.js +21 -19
- package/dist/commands/run-dag-progress.js +1 -1
- package/dist/commands/status.js +18 -17
- package/dist/commands/study-init.js +2 -2
- package/dist/commands/task-advance.js +335 -0
- package/dist/commands/task-contract.js +3 -5
- package/dist/commands/task-source-prepare.js +9 -4
- package/dist/commands/task-status.js +133 -0
- package/dist/executors/dag-pi-executor.js +18 -22
- package/dist/executors/shell-executor.js +41 -25
- package/dist/governance/manifest-types.js +2 -2
- package/dist/shared/operator/capabilities.js +1358 -243
- package/dist/task/contract/adopt.js +1 -1
- package/dist/task/contract/apply.js +1 -1
- package/dist/task/contract/import-revision.js +1 -1
- package/dist/task/contract/recover.js +2 -2
- package/dist/task/read-model.js +18 -23
- package/dist/task/runtime.js +1 -1
- package/dist/task/source-prepare/completeness.js +1 -1
- package/dist/task/source-prepare/parse-intent.js +6 -1
- package/dist/task/source-prepare/prepare.js +23 -20
- package/dist/worker/console/operator-actions.js +288 -95
- package/dist/worker/console/recovery-cta.js +4 -4
- package/dist/worker/console/static/assets/{index-CSRIhuzh.js → index-CNO7n6qB.js} +1 -1
- package/dist/worker/console/static/index.html +1 -1
- package/dist/worker/materialize/harness-task-materializer.js +6 -5
- package/dist/worker/run-task/run-task.js +204 -112
- package/dist/worker/runner/run-ready.js +1 -1
- package/dist/workflows/dag/backend-test-markdown-workflow.js +49 -23
- package/dist/workflows/dag/backend-test-pytest-collection.js +57 -9
- package/dist/workflows/dag/frontend-implementation-contract.js +3 -3
- package/dist/workflows/dag/frontend-prewrite-gate.js +18 -1
- package/dist/workflows/dag/frontend-test-l5-report.js +46 -7
- package/dist/workflows/dag/init-hybrid.js +6 -35
- package/dist/workflows/dag/output-protocol.js +23 -0
- package/docs/templates/backend-test-dag.json +2 -2
- package/docs/templates/evaluation/agents-map-slim-v1.md +1 -1
- package/docs/templates/evaluation/agents-map-verbose-v0.md +3 -3
- package/docs/templates/frontend-implementation-contract.schema.json +2 -2
- package/docs/templates/harness.schema.json +2 -2
- package/docs/templates/init-managed-agents.md +13 -16
- package/docs/templates/production-readiness-checklist.md +3 -3
- package/harness.json +1 -1
- package/package.json +1 -1
- package/scripts/kb-bootstrap-init-skeleton.sh +2 -1
- package/scripts/kb-graph-incremental-prepare.mjs +2 -2
- package/skills/loop-agent/SKILL.md +18 -13
- package/skills/loop-agent/references/README.md +1 -1
- package/skills/loop-agent/references/command-reference.md +55 -84
- package/skills/loop-agent/references/harness-policy.md +19 -23
- package/skills/loop-agent/references/hybrid-dag.md +32 -34
- package/skills/loop-agent/references/long-running-loop.md +2 -2
- package/skills/loop-agent/references/one-shot-runs.md +4 -5
- package/skills/loop-agent/references/orchestrator-and-interventions.md +3 -3
- package/skills/loop-agent/references/post-implementation-and-patterns.md +4 -4
- package/skills/loop-agent/references/source-and-plan-practice.md +45 -51
- package/skills/loop-agent/references/task-workflow.md +14 -15
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure reducer: given a lifecycle snapshot + intent flags, decide the next
|
|
3
|
+
* safe automatic transitions until a stop condition.
|
|
4
|
+
*/
|
|
5
|
+
export function planTransitions(input) {
|
|
6
|
+
const { snapshot } = input;
|
|
7
|
+
const actions = [];
|
|
8
|
+
const expectedTransitions = [];
|
|
9
|
+
const blockers = [...snapshot.blockers];
|
|
10
|
+
if (input.rejectGate) {
|
|
11
|
+
return {
|
|
12
|
+
actions: [{ type: "stop", reason: "terminal" }],
|
|
13
|
+
gate: snapshot.gate ?? null,
|
|
14
|
+
blockers,
|
|
15
|
+
next: {
|
|
16
|
+
kind: "advance",
|
|
17
|
+
command: `loop-agent task advance ${snapshot.taskId} --json`,
|
|
18
|
+
},
|
|
19
|
+
expectedTransitions: ["write-set-rejected"],
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
if (!snapshot.exists) {
|
|
23
|
+
// Create is always allowed when advance is invoked for a missing task.
|
|
24
|
+
// Further intake (PRD/boundaries) is required before contract/DAG steps.
|
|
25
|
+
actions.push({ type: "create-task" });
|
|
26
|
+
expectedTransitions.push("task-created");
|
|
27
|
+
if (!input.hasPrdInput && !input.hasEngineeringBoundary) {
|
|
28
|
+
actions.push({ type: "stop", reason: "blocker" });
|
|
29
|
+
blockers.push({
|
|
30
|
+
code: "INTAKE_REQUIRED",
|
|
31
|
+
message: "task created or creatable; provide --prd and engineering boundary flags (or --from-text) to continue",
|
|
32
|
+
});
|
|
33
|
+
return {
|
|
34
|
+
actions,
|
|
35
|
+
gate: null,
|
|
36
|
+
blockers,
|
|
37
|
+
next: {
|
|
38
|
+
kind: "advance",
|
|
39
|
+
command: `loop-agent task advance ${snapshot.taskId} --prd <path> --allowed-path <glob> --json`,
|
|
40
|
+
},
|
|
41
|
+
expectedTransitions,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (snapshot.contract?.transactionIncomplete) {
|
|
46
|
+
actions.push({ type: "recover-contract" });
|
|
47
|
+
expectedTransitions.push("contract-recovered");
|
|
48
|
+
}
|
|
49
|
+
const sourceReady = snapshot.source?.requirementReady === true;
|
|
50
|
+
const contractManaged = snapshot.contract?.effectiveStatus === "managed" ||
|
|
51
|
+
Boolean(snapshot.contract?.revision && snapshot.contract.revision > 0);
|
|
52
|
+
const alreadyImportedPrd = (snapshot.source?.importedPrdCount ?? 0) > 0;
|
|
53
|
+
// Prefer prepare once PRDs are on disk; re-import only when caller still
|
|
54
|
+
// signals hasPrdInput (advance.ts latches this after one import per call).
|
|
55
|
+
const shouldImportPrd = input.hasPrdInput;
|
|
56
|
+
if (input.forcePrepareContract) {
|
|
57
|
+
if (shouldImportPrd) {
|
|
58
|
+
actions.push({ type: "import-prd" });
|
|
59
|
+
expectedTransitions.push("prd-imported");
|
|
60
|
+
}
|
|
61
|
+
actions.push({ type: "prepare-contract" });
|
|
62
|
+
expectedTransitions.push("contract-managed");
|
|
63
|
+
}
|
|
64
|
+
else if (!sourceReady || !contractManaged) {
|
|
65
|
+
if ((shouldImportPrd || alreadyImportedPrd) &&
|
|
66
|
+
!input.hasEngineeringBoundary) {
|
|
67
|
+
// Console bootstrap / import-only: archive PRD(s), do not prepare contract yet.
|
|
68
|
+
if (shouldImportPrd) {
|
|
69
|
+
actions.push({ type: "import-prd" });
|
|
70
|
+
expectedTransitions.push("prd-imported");
|
|
71
|
+
}
|
|
72
|
+
actions.push({ type: "stop", reason: "blocker" });
|
|
73
|
+
blockers.push({
|
|
74
|
+
code: "BOUNDARY_REQUIRED",
|
|
75
|
+
message: "PRD imported; provide --allowed-path / --verify (or --from-text with boundaries) to prepare contract and open writeSet gate",
|
|
76
|
+
});
|
|
77
|
+
return {
|
|
78
|
+
actions,
|
|
79
|
+
gate: null,
|
|
80
|
+
blockers,
|
|
81
|
+
next: {
|
|
82
|
+
kind: "advance",
|
|
83
|
+
command: `loop-agent task advance ${snapshot.taskId} --allowed-path <glob> --verify <label:command> --json`,
|
|
84
|
+
},
|
|
85
|
+
expectedTransitions,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
if (shouldImportPrd ||
|
|
89
|
+
input.hasEngineeringBoundary ||
|
|
90
|
+
alreadyImportedPrd ||
|
|
91
|
+
snapshot.exists) {
|
|
92
|
+
if (shouldImportPrd) {
|
|
93
|
+
actions.push({ type: "import-prd" });
|
|
94
|
+
expectedTransitions.push("prd-imported");
|
|
95
|
+
}
|
|
96
|
+
actions.push({ type: "prepare-contract" });
|
|
97
|
+
expectedTransitions.push("contract-managed");
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
blockers.push({
|
|
101
|
+
code: "CONTRACT_INCOMPLETE",
|
|
102
|
+
message: "source/contract not ready; re-run advance with PRD and boundaries",
|
|
103
|
+
});
|
|
104
|
+
return stopWith(actions, expectedTransitions, blockers, snapshot, "blocker");
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const dagExists = snapshot.dagDraft?.exists === true;
|
|
108
|
+
const strictValidated = snapshot.dagDraft?.strictValidated === true;
|
|
109
|
+
if (!dagExists) {
|
|
110
|
+
actions.push({ type: "generate-dag" });
|
|
111
|
+
expectedTransitions.push("dag-generated");
|
|
112
|
+
}
|
|
113
|
+
if (!strictValidated) {
|
|
114
|
+
actions.push({ type: "strict-validate-dag" });
|
|
115
|
+
expectedTransitions.push("dag-strict-validated");
|
|
116
|
+
}
|
|
117
|
+
// After validate, open write-set gate unless already approved/rejected for current digest.
|
|
118
|
+
const gate = snapshot.gate ?? null;
|
|
119
|
+
const postRunState = snapshot.lifecycleState === "running" ||
|
|
120
|
+
snapshot.lifecycleState === "run-succeeded" ||
|
|
121
|
+
snapshot.lifecycleState === "run-failed" ||
|
|
122
|
+
snapshot.lifecycleState === "evidence-closed" ||
|
|
123
|
+
snapshot.lifecycleState === "needs-attention" ||
|
|
124
|
+
snapshot.lifecycleState === "awaiting-decision";
|
|
125
|
+
const approvedForCurrent = postRunState || (input.approveGate && gate !== null);
|
|
126
|
+
if (!approvedForCurrent && (strictValidated || !dagExists)) {
|
|
127
|
+
// Gate opens after generate+validate in the advance loop; plan includes open.
|
|
128
|
+
if (!input.approveGate) {
|
|
129
|
+
actions.push({ type: "open-write-set-gate" });
|
|
130
|
+
expectedTransitions.push("write-set-gate-opened");
|
|
131
|
+
if (input.dryRun) {
|
|
132
|
+
actions.push({ type: "stop", reason: "dry-run" });
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
actions.push({ type: "stop", reason: "gate" });
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
actions,
|
|
139
|
+
gate,
|
|
140
|
+
blockers,
|
|
141
|
+
next: gate
|
|
142
|
+
? { kind: "approve-gate", command: gate.approveCommand }
|
|
143
|
+
: {
|
|
144
|
+
kind: "advance",
|
|
145
|
+
command: `loop-agent task advance ${snapshot.taskId} --json`,
|
|
146
|
+
},
|
|
147
|
+
expectedTransitions,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// Only start/monitor when not already in a terminal post-run state.
|
|
152
|
+
const alreadyTerminalRun = snapshot.lifecycleState === "run-succeeded" ||
|
|
153
|
+
snapshot.lifecycleState === "run-failed" ||
|
|
154
|
+
snapshot.lifecycleState === "evidence-closed";
|
|
155
|
+
if (!alreadyTerminalRun) {
|
|
156
|
+
if (input.approveGate && !postRunState) {
|
|
157
|
+
expectedTransitions.push("write-set-approved");
|
|
158
|
+
actions.push({ type: "start-or-monitor-run" });
|
|
159
|
+
expectedTransitions.push("dag-run-started", "dag-run-monitored");
|
|
160
|
+
}
|
|
161
|
+
else if (snapshot.lifecycleState === "running" ||
|
|
162
|
+
snapshot.activeRun ||
|
|
163
|
+
(input.approveGate && Boolean(snapshot.activeRun))) {
|
|
164
|
+
actions.push({ type: "start-or-monitor-run" });
|
|
165
|
+
expectedTransitions.push("dag-run-monitored");
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (!input.skipFinalize &&
|
|
169
|
+
(snapshot.lifecycleState === "run-succeeded" ||
|
|
170
|
+
(snapshot.latestRun?.lifecycle === "completed" &&
|
|
171
|
+
snapshot.latestRun.status === "finished"))) {
|
|
172
|
+
if (!snapshot.promotion?.ready) {
|
|
173
|
+
actions.push({ type: "promote" });
|
|
174
|
+
expectedTransitions.push("promotion-completed");
|
|
175
|
+
}
|
|
176
|
+
if (!snapshot.closeout?.exists) {
|
|
177
|
+
actions.push({ type: "closeout" });
|
|
178
|
+
expectedTransitions.push("closeout-completed");
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (snapshot.lifecycleState === "evidence-closed") {
|
|
182
|
+
return {
|
|
183
|
+
actions: [{ type: "stop", reason: "terminal" }],
|
|
184
|
+
gate: null,
|
|
185
|
+
blockers,
|
|
186
|
+
next: { kind: "none" },
|
|
187
|
+
expectedTransitions: [],
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
if (input.dryRun) {
|
|
191
|
+
actions.push({ type: "stop", reason: "dry-run" });
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
actions,
|
|
195
|
+
gate,
|
|
196
|
+
blockers,
|
|
197
|
+
next: deriveNext(snapshot, gate, input.approveGate),
|
|
198
|
+
expectedTransitions,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
function stopWith(actions, expectedTransitions, blockers, snapshot, reason) {
|
|
202
|
+
return {
|
|
203
|
+
actions: [...actions, { type: "stop", reason }],
|
|
204
|
+
gate: snapshot.gate ?? null,
|
|
205
|
+
blockers,
|
|
206
|
+
next: {
|
|
207
|
+
kind: "advance",
|
|
208
|
+
command: `loop-agent task advance ${snapshot.taskId} --json`,
|
|
209
|
+
},
|
|
210
|
+
expectedTransitions,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function deriveNext(snapshot, gate, approveGate) {
|
|
214
|
+
if (gate && !approveGate) {
|
|
215
|
+
return { kind: "approve-gate", command: gate.approveCommand };
|
|
216
|
+
}
|
|
217
|
+
if (snapshot.lifecycleState === "evidence-closed") {
|
|
218
|
+
return { kind: "none" };
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
kind: "advance",
|
|
222
|
+
command: `loop-agent task advance ${snapshot.taskId} --json`,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { access, readFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { loadTaskConfig } from "../../task/runtime.js";
|
|
5
|
+
function sha256Canonical(value) {
|
|
6
|
+
const json = JSON.stringify(value);
|
|
7
|
+
return `sha256:${createHash("sha256").update(json, "utf8").digest("hex")}`;
|
|
8
|
+
}
|
|
9
|
+
async function pathExists(filePath) {
|
|
10
|
+
try {
|
|
11
|
+
await access(filePath);
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async function readPackageScripts(repoRoot) {
|
|
19
|
+
const packageJsonPath = path.join(repoRoot, "package.json");
|
|
20
|
+
if (!(await pathExists(packageJsonPath)))
|
|
21
|
+
return {};
|
|
22
|
+
try {
|
|
23
|
+
const raw = await readFile(packageJsonPath, "utf-8");
|
|
24
|
+
const parsed = JSON.parse(raw);
|
|
25
|
+
return parsed.scripts ?? {};
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Deterministic engineering-boundary recommendations (P1).
|
|
33
|
+
*
|
|
34
|
+
* Priority:
|
|
35
|
+
* 1. managed task config (highest)
|
|
36
|
+
* 2. package.json scripts (typecheck/test/check-repo)
|
|
37
|
+
* 3. PRD path-like tokens as weak candidates only (never auto-elevated)
|
|
38
|
+
*
|
|
39
|
+
* Recommendations never enter contract draft unless acceptDigest matches.
|
|
40
|
+
*/
|
|
41
|
+
export async function buildTaskLifecycleRecommendations(input) {
|
|
42
|
+
const allowedPaths = [];
|
|
43
|
+
const verifyCommands = [];
|
|
44
|
+
// 1) managed task config
|
|
45
|
+
try {
|
|
46
|
+
const config = await loadTaskConfig(input.repoRoot, input.taskId);
|
|
47
|
+
for (const glob of config.allowedPaths ?? []) {
|
|
48
|
+
if (!glob.trim())
|
|
49
|
+
continue;
|
|
50
|
+
allowedPaths.push({
|
|
51
|
+
glob: glob.trim(),
|
|
52
|
+
reason: "existing managed task.json.allowedPaths",
|
|
53
|
+
source: "managed-task-config",
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
for (const cmd of config.verifyCommands ?? []) {
|
|
57
|
+
const label = cmd.label?.trim() || "verify";
|
|
58
|
+
const command = cmd.command?.trim();
|
|
59
|
+
if (!command)
|
|
60
|
+
continue;
|
|
61
|
+
verifyCommands.push({
|
|
62
|
+
label,
|
|
63
|
+
command,
|
|
64
|
+
reason: "existing managed task.json.verifyCommands",
|
|
65
|
+
source: "managed-task-config",
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// task may not exist yet
|
|
71
|
+
}
|
|
72
|
+
// 2) package.json scripts (only if not already covered by managed config)
|
|
73
|
+
const scripts = await readPackageScripts(input.repoRoot);
|
|
74
|
+
const hasVerifyLabel = (label) => verifyCommands.some((entry) => entry.label === label);
|
|
75
|
+
const scriptCandidates = [
|
|
76
|
+
{ label: "typecheck", script: "typecheck" },
|
|
77
|
+
{ label: "test", script: "test" },
|
|
78
|
+
{ label: "check-repo", script: "check:repo" },
|
|
79
|
+
{ label: "check-repo", script: "check-repo" },
|
|
80
|
+
];
|
|
81
|
+
for (const candidate of scriptCandidates) {
|
|
82
|
+
const scriptBody = scripts[candidate.script];
|
|
83
|
+
if (!scriptBody)
|
|
84
|
+
continue;
|
|
85
|
+
if (hasVerifyLabel(candidate.label))
|
|
86
|
+
continue;
|
|
87
|
+
verifyCommands.push({
|
|
88
|
+
label: candidate.label,
|
|
89
|
+
command: `npm run ${candidate.script}`,
|
|
90
|
+
reason: `package.json scripts.${candidate.script}`,
|
|
91
|
+
source: "package-scripts",
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
// 3) weak PRD path candidates (never auto-accepted alone)
|
|
95
|
+
const prdTexts = input.prdTexts ?? [];
|
|
96
|
+
const seenGlobs = new Set(allowedPaths.map((entry) => entry.glob));
|
|
97
|
+
const pathLike = /(?:^|[\s`"'(])((?:src|test|tests|docs|website|scripts|skills)\/[A-Za-z0-9_./ conf-]+(?:\/\*\*)?)/gm;
|
|
98
|
+
for (const text of prdTexts) {
|
|
99
|
+
for (const match of text.matchAll(pathLike)) {
|
|
100
|
+
const raw = match[1]?.trim();
|
|
101
|
+
if (!raw)
|
|
102
|
+
continue;
|
|
103
|
+
const glob = raw.endsWith("/**")
|
|
104
|
+
? raw
|
|
105
|
+
: raw.endsWith("/")
|
|
106
|
+
? `${raw}**`
|
|
107
|
+
: raw.includes("*")
|
|
108
|
+
? raw
|
|
109
|
+
: `${raw.replace(/\/$/, "")}/**`;
|
|
110
|
+
if (seenGlobs.has(glob))
|
|
111
|
+
continue;
|
|
112
|
+
// Skip overly broad roots
|
|
113
|
+
if (glob === "src/**" || glob === "docs/**" || glob === "test/**") {
|
|
114
|
+
// still allow as weak candidate with explicit reason
|
|
115
|
+
}
|
|
116
|
+
seenGlobs.add(glob);
|
|
117
|
+
allowedPaths.push({
|
|
118
|
+
glob,
|
|
119
|
+
reason: "path-like token in PRD (weak candidate; not auto-elevated)",
|
|
120
|
+
source: "prd-weak-candidate",
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// Stable sort for digest
|
|
125
|
+
allowedPaths.sort((a, b) => a.glob.localeCompare(b.glob));
|
|
126
|
+
verifyCommands.sort((a, b) => a.label.localeCompare(b.label));
|
|
127
|
+
const digestPayload = {
|
|
128
|
+
schemaVersion: 1,
|
|
129
|
+
taskId: input.taskId,
|
|
130
|
+
allowedPaths: allowedPaths.map((entry) => ({
|
|
131
|
+
glob: entry.glob,
|
|
132
|
+
source: entry.source,
|
|
133
|
+
})),
|
|
134
|
+
verifyCommands: verifyCommands.map((entry) => ({
|
|
135
|
+
label: entry.label,
|
|
136
|
+
command: entry.command,
|
|
137
|
+
source: entry.source,
|
|
138
|
+
})),
|
|
139
|
+
};
|
|
140
|
+
const digest = sha256Canonical(digestPayload);
|
|
141
|
+
return {
|
|
142
|
+
allowedPaths,
|
|
143
|
+
verifyCommands,
|
|
144
|
+
digest,
|
|
145
|
+
requiresAccept: true,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
export function applyAcceptedRecommendations(input) {
|
|
149
|
+
const explicitAllowed = input.explicitAllowedPaths ?? [];
|
|
150
|
+
const explicitVerify = input.explicitVerifyCommands ?? [];
|
|
151
|
+
if (explicitAllowed.length > 0 || explicitVerify.length > 0) {
|
|
152
|
+
return {
|
|
153
|
+
allowedPaths: explicitAllowed.length > 0 ? explicitAllowed : undefined,
|
|
154
|
+
verifyCommands: explicitVerify.length > 0 ? explicitVerify : undefined,
|
|
155
|
+
accepted: false,
|
|
156
|
+
staleAccept: false,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
const accept = input.acceptDigest?.trim();
|
|
160
|
+
if (!accept) {
|
|
161
|
+
return { accepted: false, staleAccept: false };
|
|
162
|
+
}
|
|
163
|
+
if (accept !== input.recommendations.digest) {
|
|
164
|
+
return { accepted: false, staleAccept: true };
|
|
165
|
+
}
|
|
166
|
+
// Only elevate non-weak sources automatically when accepted.
|
|
167
|
+
const allowedPaths = input.recommendations.allowedPaths
|
|
168
|
+
.filter((entry) => entry.source !== "prd-weak-candidate")
|
|
169
|
+
.map((entry) => entry.glob);
|
|
170
|
+
const verifyCommands = input.recommendations.verifyCommands.map((entry) => ({
|
|
171
|
+
label: entry.label,
|
|
172
|
+
command: entry.command,
|
|
173
|
+
}));
|
|
174
|
+
return {
|
|
175
|
+
allowedPaths: allowedPaths.length > 0 ? allowedPaths : undefined,
|
|
176
|
+
verifyCommands: verifyCommands.length > 0 ? verifyCommands : undefined,
|
|
177
|
+
accepted: true,
|
|
178
|
+
staleAccept: false,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { access, mkdir, readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { writeJsonAtomic } from "../../infrastructure/harness/atomic-write.js";
|
|
4
|
+
import { getTaskDir } from "../../task/runtime.js";
|
|
5
|
+
export function getLifecycleRecordPath(repoRoot, taskId) {
|
|
6
|
+
return path.join(getTaskDir(repoRoot, taskId), "lifecycle.json");
|
|
7
|
+
}
|
|
8
|
+
async function exists(filePath) {
|
|
9
|
+
try {
|
|
10
|
+
await access(filePath);
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export async function loadLifecycleRecord(repoRoot, taskId) {
|
|
18
|
+
const filePath = getLifecycleRecordPath(repoRoot, taskId);
|
|
19
|
+
if (!(await exists(filePath)))
|
|
20
|
+
return null;
|
|
21
|
+
try {
|
|
22
|
+
const raw = JSON.parse(await readFile(filePath, "utf-8"));
|
|
23
|
+
if (raw.schemaVersion !== 1 || raw.taskId !== taskId)
|
|
24
|
+
return null;
|
|
25
|
+
return raw;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function emptyLifecycleRecord(taskId) {
|
|
32
|
+
return {
|
|
33
|
+
schemaVersion: 1,
|
|
34
|
+
taskId,
|
|
35
|
+
updatedAt: new Date().toISOString(),
|
|
36
|
+
completedTransitions: [],
|
|
37
|
+
runAssociations: [],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export async function saveLifecycleRecord(repoRoot, record) {
|
|
41
|
+
const filePath = getLifecycleRecordPath(repoRoot, record.taskId);
|
|
42
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
43
|
+
const next = {
|
|
44
|
+
...record,
|
|
45
|
+
updatedAt: new Date().toISOString(),
|
|
46
|
+
};
|
|
47
|
+
await writeJsonAtomic(filePath, next);
|
|
48
|
+
}
|
|
49
|
+
export function appendTransition(record, transition) {
|
|
50
|
+
if (record.completedTransitions.includes(transition)) {
|
|
51
|
+
return record;
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
...record,
|
|
55
|
+
completedTransitions: [...record.completedTransitions, transition],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export function recordRunAssociation(record, run) {
|
|
59
|
+
if (record.runAssociations.some((entry) => entry.runId === run.runId)) {
|
|
60
|
+
return record;
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
...record,
|
|
64
|
+
runAssociations: [
|
|
65
|
+
...record.runAssociations,
|
|
66
|
+
{
|
|
67
|
+
runId: run.runId,
|
|
68
|
+
recordedAt: new Date().toISOString(),
|
|
69
|
+
source: run.source,
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const TASK_LIFECYCLE_SCHEMA_VERSION = 1;
|
|
@@ -5,21 +5,16 @@ import { runEval } from "../commands/eval.js";
|
|
|
5
5
|
import { runCoverageAudit } from "../commands/coverage-audit.js";
|
|
6
6
|
import { runCoverageReport } from "../commands/coverage-report.js";
|
|
7
7
|
import { runExamples } from "../commands/examples.js";
|
|
8
|
-
import { runCloseout } from "../commands/closeout.js";
|
|
9
8
|
import { runHandoffCheck } from "../commands/handoff-check.js";
|
|
10
9
|
import { runInspect } from "../commands/inspect.js";
|
|
11
10
|
import { runInstructions } from "../commands/instructions.js";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import { runTaskContract } from "../commands/task-contract.js";
|
|
15
|
-
import { runTaskSourcePrepare } from "../commands/task-source-prepare.js";
|
|
11
|
+
import { runTaskAdvance } from "../commands/task-advance.js";
|
|
12
|
+
import { runTaskStatus } from "../commands/task-status.js";
|
|
16
13
|
import { runOperator } from "../commands/operator.js";
|
|
17
14
|
import { runPlanList } from "../commands/plan-list.js";
|
|
18
15
|
import { runPlanCheck, runPlanComplete, runPlanCreate, } from "../commands/plan.js";
|
|
19
|
-
import { runPromoteRun } from "../commands/promote-run.js";
|
|
20
16
|
import { runSpine } from "../commands/spine.js";
|
|
21
17
|
import { runStats } from "../commands/stats.js";
|
|
22
|
-
import { runStatus } from "../commands/status.js";
|
|
23
18
|
import { runWorktreeCreate } from "../commands/worktree-create.js";
|
|
24
19
|
import { runWorktreeList } from "../commands/worktree-list.js";
|
|
25
20
|
import { runWorktreeRemove } from "../commands/worktree-remove.js";
|
|
@@ -42,7 +37,6 @@ import { runDagRerun } from "../commands/dag-rerun.js";
|
|
|
42
37
|
import { runDagResume } from "../commands/dag-resume.js";
|
|
43
38
|
import { runDagDecisionInspect, runDagDecisionValidate, } from "../workflows/dag/decision-envelope.js";
|
|
44
39
|
import { runDagDoctor, runDagStatus } from "../workflows/dag/lifecycle.js";
|
|
45
|
-
import { runDagRunTask } from "../commands/dag-run-task.js";
|
|
46
40
|
import { runDagCloseoutDraft, runDagReport } from "../commands/dag-report.js";
|
|
47
41
|
import { runDagFinalVerification } from "../commands/dag-final-verification.js";
|
|
48
42
|
import { runKnowledge } from "../commands/knowledge.js";
|
|
@@ -121,7 +115,7 @@ const KNOWLEDGE_SUBCOMMANDS = [
|
|
|
121
115
|
];
|
|
122
116
|
const DAG_SUBCOMMANDS = [
|
|
123
117
|
"init-hybrid",
|
|
124
|
-
"
|
|
118
|
+
"execute",
|
|
125
119
|
"validate",
|
|
126
120
|
"workflow-plan",
|
|
127
121
|
"workflow-validate",
|
|
@@ -140,17 +134,7 @@ const DAG_SUBCOMMANDS = [
|
|
|
140
134
|
"final-verification",
|
|
141
135
|
"decision",
|
|
142
136
|
];
|
|
143
|
-
const TASK_SUBCOMMANDS = ["
|
|
144
|
-
const TASK_CONTRACT_SUBCOMMANDS = [
|
|
145
|
-
"show",
|
|
146
|
-
"validate",
|
|
147
|
-
"diff",
|
|
148
|
-
"apply",
|
|
149
|
-
"adopt",
|
|
150
|
-
"doctor",
|
|
151
|
-
"recover",
|
|
152
|
-
];
|
|
153
|
-
const TASK_SOURCE_SUBCOMMANDS = ["prepare"];
|
|
137
|
+
const TASK_SUBCOMMANDS = ["advance", "status"];
|
|
154
138
|
const OPERATOR_SUBCOMMANDS = ["capabilities"];
|
|
155
139
|
const WORKFLOW_SUBCOMMANDS = [
|
|
156
140
|
"list",
|
|
@@ -246,57 +230,21 @@ export const COMMAND_DEFINITIONS = [
|
|
|
246
230
|
await runEval(repoRoot, [subcommand, ...rest].filter((arg) => Boolean(arg)));
|
|
247
231
|
},
|
|
248
232
|
},
|
|
249
|
-
{
|
|
250
|
-
name: "new-task",
|
|
251
|
-
adapter: "required",
|
|
252
|
-
tier: "primary",
|
|
253
|
-
intent: "Create a harness task before DAG autonomous work.",
|
|
254
|
-
usage: "new-task <task-id> [title]",
|
|
255
|
-
handler: async ({ repoRoot, subcommand, rest }) => {
|
|
256
|
-
const [taskId, ...titleParts] = [subcommand, ...rest];
|
|
257
|
-
if (!taskId)
|
|
258
|
-
throw new Error("taskId required");
|
|
259
|
-
await runNewTask(repoRoot, taskId, titleParts.join(" ") || undefined);
|
|
260
|
-
},
|
|
261
|
-
},
|
|
262
|
-
{
|
|
263
|
-
name: "import-prd",
|
|
264
|
-
adapter: "required",
|
|
265
|
-
tier: "primary",
|
|
266
|
-
intent: "Copy a user PRD into source/references as an immutable fact source before deriving source/需求.md.",
|
|
267
|
-
usage: "import-prd <task-id> --file <path> [--name requirement] [--role requirement] [--json]",
|
|
268
|
-
handler: async ({ repoRoot, subcommand, rest }) => {
|
|
269
|
-
await runImportPrd(repoRoot, [subcommand, ...rest].filter(Boolean));
|
|
270
|
-
},
|
|
271
|
-
},
|
|
272
233
|
{
|
|
273
234
|
name: "task",
|
|
274
235
|
adapter: "required",
|
|
275
236
|
tier: "primary",
|
|
276
|
-
intent: "Task
|
|
277
|
-
usage: "task <
|
|
237
|
+
intent: "Task lifecycle: advance (mutation) and status (read).",
|
|
238
|
+
usage: "task <advance|status> ...",
|
|
278
239
|
subcommands: [...TASK_SUBCOMMANDS],
|
|
279
240
|
handler: async ({ repoRoot, subcommand, rest }) => {
|
|
280
|
-
if (subcommand === "
|
|
281
|
-
|
|
282
|
-
if (!contractSub ||
|
|
283
|
-
!TASK_CONTRACT_SUBCOMMANDS.includes(contractSub)) {
|
|
284
|
-
throw new Error(`usage: task contract <${TASK_CONTRACT_SUBCOMMANDS.join("|")}> ...`);
|
|
285
|
-
}
|
|
286
|
-
await runTaskContract(repoRoot, [contractSub, ...contractRest]);
|
|
241
|
+
if (subcommand === "advance") {
|
|
242
|
+
await runTaskAdvance(repoRoot, rest);
|
|
287
243
|
return;
|
|
288
244
|
}
|
|
289
|
-
if (subcommand === "
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
!TASK_SOURCE_SUBCOMMANDS.includes(sourceSub)) {
|
|
293
|
-
throw new Error(`usage: task source <${TASK_SOURCE_SUBCOMMANDS.join("|")}> ...`);
|
|
294
|
-
}
|
|
295
|
-
if (sourceSub === "prepare") {
|
|
296
|
-
await runTaskSourcePrepare(repoRoot, sourceRest);
|
|
297
|
-
return;
|
|
298
|
-
}
|
|
299
|
-
throw new Error(`usage: task source <${TASK_SOURCE_SUBCOMMANDS.join("|")}> ...`);
|
|
245
|
+
if (subcommand === "status") {
|
|
246
|
+
await runTaskStatus(repoRoot, rest);
|
|
247
|
+
return;
|
|
300
248
|
}
|
|
301
249
|
throw new Error(formatSubcommandUsageError("task", TASK_SUBCOMMANDS));
|
|
302
250
|
},
|
|
@@ -312,19 +260,6 @@ export const COMMAND_DEFINITIONS = [
|
|
|
312
260
|
await runOperator(repoRoot, [subcommand, ...rest].filter((arg) => Boolean(arg)));
|
|
313
261
|
},
|
|
314
262
|
},
|
|
315
|
-
{
|
|
316
|
-
name: "status",
|
|
317
|
-
adapter: "required",
|
|
318
|
-
tier: "operator",
|
|
319
|
-
intent: "Inspect a task state and machine-readable action context.",
|
|
320
|
-
usage: "status <task-id> [--json]",
|
|
321
|
-
handler: async ({ repoRoot, subcommand }) => {
|
|
322
|
-
const taskId = subcommand;
|
|
323
|
-
if (!taskId)
|
|
324
|
-
throw new Error("taskId required");
|
|
325
|
-
await runStatus(repoRoot, taskId);
|
|
326
|
-
},
|
|
327
|
-
},
|
|
328
263
|
{
|
|
329
264
|
name: "instructions",
|
|
330
265
|
adapter: "required",
|
|
@@ -335,27 +270,6 @@ export const COMMAND_DEFINITIONS = [
|
|
|
335
270
|
await runInstructions(repoRoot, [subcommand, ...rest].filter(Boolean));
|
|
336
271
|
},
|
|
337
272
|
},
|
|
338
|
-
{
|
|
339
|
-
name: "promote-run",
|
|
340
|
-
adapter: "required",
|
|
341
|
-
tier: "operator",
|
|
342
|
-
intent: "Promote completed DAG or one-shot run evidence into task artifacts without mutating run facts.",
|
|
343
|
-
usage: "promote-run <task-id> --run-id <run-id>",
|
|
344
|
-
handler: async ({ repoRoot, subcommand, rest }) => {
|
|
345
|
-
await runPromoteRun(repoRoot, [subcommand, ...rest].filter(Boolean));
|
|
346
|
-
},
|
|
347
|
-
},
|
|
348
|
-
{
|
|
349
|
-
name: "closeout",
|
|
350
|
-
adapter: "required",
|
|
351
|
-
tier: "operator",
|
|
352
|
-
intent: "Create deterministic task closeout/progress records from promoted task artifacts.",
|
|
353
|
-
usage: "closeout task <task-id>",
|
|
354
|
-
subcommands: [...CLOSEOUT_SUBCOMMANDS],
|
|
355
|
-
handler: async ({ repoRoot, subcommand, rest }) => {
|
|
356
|
-
await runCloseout(repoRoot, [subcommand, ...rest].filter(Boolean));
|
|
357
|
-
},
|
|
358
|
-
},
|
|
359
273
|
{
|
|
360
274
|
name: "stats",
|
|
361
275
|
adapter: "required",
|
|
@@ -614,9 +528,9 @@ export const COMMAND_DEFINITIONS = [
|
|
|
614
528
|
await runDagDoctor(repoRoot, doctorArgs);
|
|
615
529
|
return;
|
|
616
530
|
}
|
|
617
|
-
if (subcommand === "
|
|
618
|
-
const
|
|
619
|
-
await
|
|
531
|
+
if (subcommand === "execute") {
|
|
532
|
+
const executeArgs = rest.filter((arg) => Boolean(arg));
|
|
533
|
+
await runRunDag(repoRoot, executeArgs);
|
|
620
534
|
return;
|
|
621
535
|
}
|
|
622
536
|
if (subcommand === "report") {
|
|
@@ -672,17 +586,6 @@ export const COMMAND_DEFINITIONS = [
|
|
|
672
586
|
throw new Error(formatSubcommandUsageError("dag", DAG_SUBCOMMANDS));
|
|
673
587
|
},
|
|
674
588
|
},
|
|
675
|
-
{
|
|
676
|
-
name: "run-dag",
|
|
677
|
-
adapter: "required",
|
|
678
|
-
tier: "primary",
|
|
679
|
-
intent: "Execute a reviewed Agent DAG spec.",
|
|
680
|
-
usage: "run-dag --dag <path> [--cwd <dir>] [--init-only] [--dry-run] [--max-concurrent N] [--run-id id] [--quiet] [--progress-interval-ms N] [--canvas-path <abs-path> | --canvas <name> [--canvases-dir <dir>]] [--events-jsonl <path>] [--worker-association <json>]",
|
|
681
|
-
handler: async ({ repoRoot, subcommand, rest }) => {
|
|
682
|
-
const runDagArgs = [subcommand, ...rest].filter((arg) => Boolean(arg));
|
|
683
|
-
await runRunDag(repoRoot, runDagArgs);
|
|
684
|
-
},
|
|
685
|
-
},
|
|
686
589
|
{
|
|
687
590
|
name: "workflow",
|
|
688
591
|
adapter: "required",
|