@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,80 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
export function normalizeWriteSet(writeSet) {
|
|
3
|
+
return [
|
|
4
|
+
...new Set(writeSet.map((entry) => entry.replace(/\\/g, "/").trim()).filter(Boolean)),
|
|
5
|
+
].sort((a, b) => a.localeCompare(b));
|
|
6
|
+
}
|
|
7
|
+
export function stableStringify(value) {
|
|
8
|
+
if (value === null || typeof value !== "object") {
|
|
9
|
+
return JSON.stringify(value);
|
|
10
|
+
}
|
|
11
|
+
if (Array.isArray(value)) {
|
|
12
|
+
return `[${value.map((item) => stableStringify(item)).join(",")}]`;
|
|
13
|
+
}
|
|
14
|
+
const record = value;
|
|
15
|
+
const keys = Object.keys(record).sort();
|
|
16
|
+
return `{${keys
|
|
17
|
+
.map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`)
|
|
18
|
+
.join(",")}}`;
|
|
19
|
+
}
|
|
20
|
+
export function sha256Hex(input) {
|
|
21
|
+
return createHash("sha256").update(input, "utf8").digest("hex");
|
|
22
|
+
}
|
|
23
|
+
export function hashCanonicalPayload(value) {
|
|
24
|
+
return `sha256:${sha256Hex(stableStringify(value))}`;
|
|
25
|
+
}
|
|
26
|
+
export function buildGateBinding(input) {
|
|
27
|
+
return {
|
|
28
|
+
taskId: input.taskId,
|
|
29
|
+
contractRevision: input.contractRevision,
|
|
30
|
+
contractCanonicalHash: input.contractCanonicalHash,
|
|
31
|
+
sourceBindingHash: input.sourceBindingHash,
|
|
32
|
+
dagSpecHash: input.dagSpecHash,
|
|
33
|
+
writeSet: normalizeWriteSet(input.writeSet),
|
|
34
|
+
controllerIdentityAnchor: input.controllerIdentityAnchor,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export function computeGateDigest(bound) {
|
|
38
|
+
return hashCanonicalPayload({
|
|
39
|
+
kind: "task-lifecycle-gate",
|
|
40
|
+
version: 1,
|
|
41
|
+
...bound,
|
|
42
|
+
writeSet: normalizeWriteSet(bound.writeSet),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
export function buildWriteSetGate(input) {
|
|
46
|
+
const digest = computeGateDigest(input.bound);
|
|
47
|
+
const id = "write-set-review";
|
|
48
|
+
return {
|
|
49
|
+
id,
|
|
50
|
+
type: "human-approval",
|
|
51
|
+
digest,
|
|
52
|
+
writeSet: normalizeWriteSet(input.bound.writeSet),
|
|
53
|
+
rationale: input.rationale ?? "validated DAG is ready; writer has not started",
|
|
54
|
+
approveCommand: `loop-agent task advance ${input.taskId} --approve-gate ${id}:${digest} --json`,
|
|
55
|
+
rejectCommand: `loop-agent task advance ${input.taskId} --reject-gate ${id}:${digest} --reason <text> --json`,
|
|
56
|
+
bound: input.bound,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
export function parseGateToken(token) {
|
|
60
|
+
const separator = token.indexOf(":");
|
|
61
|
+
if (separator <= 0 || separator === token.length - 1) {
|
|
62
|
+
throw new Error(`invalid gate token ${token}; expected <gate-id>:<digest>`);
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
id: token.slice(0, separator),
|
|
66
|
+
digest: token.slice(separator + 1),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
export function gateMatches(gate, token) {
|
|
70
|
+
return gate.id === token.id && gate.digest === token.digest;
|
|
71
|
+
}
|
|
72
|
+
export function controllerIdentityAnchor(input) {
|
|
73
|
+
if (!input?.packageName || !input.packageVersion)
|
|
74
|
+
return null;
|
|
75
|
+
return [
|
|
76
|
+
input.packageName,
|
|
77
|
+
input.packageVersion,
|
|
78
|
+
input.packageFingerprintValue ?? "",
|
|
79
|
+
].join("@");
|
|
80
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export * from "./types.js";
|
|
2
|
+
export * from "./gates.js";
|
|
3
|
+
export * from "./record.js";
|
|
4
|
+
export * from "./observe.js";
|
|
5
|
+
export * from "./plan-transitions.js";
|
|
6
|
+
export { buildTaskLifecycleRecommendations, applyAcceptedRecommendations, } from "./recommendations.js";
|
|
7
|
+
export * from "./advance.js";
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
import { access, readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { getTaskDir, getTaskPaths, getTaskStatus, loadTaskConfig, } from "../../task/runtime.js";
|
|
4
|
+
import { logicalRevisionForState, observeTaskContractState, } from "../../task/contract/index.js";
|
|
5
|
+
import { assessDagRunLiveness, listAllDagRunEntries, readDagRunSpec, readDagRunState, } from "../../workflows/dag/lifecycle.js";
|
|
6
|
+
import { resolveRunningControllerIdentity } from "../../shared/package-metadata.js";
|
|
7
|
+
import { loadLifecycleRecord } from "./record.js";
|
|
8
|
+
import { buildGateBinding, buildWriteSetGate, controllerIdentityAnchor, hashCanonicalPayload, normalizeWriteSet, sha256Hex, } from "./gates.js";
|
|
9
|
+
async function exists(filePath) {
|
|
10
|
+
try {
|
|
11
|
+
await access(filePath);
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function closeoutPath(repoRoot, taskId) {
|
|
19
|
+
const slug = taskId.replace(/^\d{4}-\d{2}-\d{2}-/, "");
|
|
20
|
+
const datePrefix = taskId.match(/^\d{4}-\d{2}-\d{2}/)?.[0] ??
|
|
21
|
+
new Date().toISOString().slice(0, 10);
|
|
22
|
+
return path.join(repoRoot, ".harness", "task-pool", "artifacts", "closeout", `${datePrefix}-${slug}.md`);
|
|
23
|
+
}
|
|
24
|
+
function collectWriterWriteSet(spec) {
|
|
25
|
+
const defaults = spec.defaults?.writePolicy ?? "none";
|
|
26
|
+
const entries = [];
|
|
27
|
+
for (const task of spec.tasks) {
|
|
28
|
+
const writePolicy = task.writePolicy ?? defaults;
|
|
29
|
+
if (writePolicy !== "exclusive")
|
|
30
|
+
continue;
|
|
31
|
+
for (const entry of task.writeSet ?? []) {
|
|
32
|
+
if (typeof entry === "string" && entry.trim())
|
|
33
|
+
entries.push(entry);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return normalizeWriteSet(entries);
|
|
37
|
+
}
|
|
38
|
+
function hashDagSpecBytes(raw) {
|
|
39
|
+
return `sha256:${sha256Hex(raw)}`;
|
|
40
|
+
}
|
|
41
|
+
function hashSourceBinding(spec) {
|
|
42
|
+
if (!spec.sourceBinding)
|
|
43
|
+
return null;
|
|
44
|
+
return hashCanonicalPayload(spec.sourceBinding);
|
|
45
|
+
}
|
|
46
|
+
async function taskExists(repoRoot, taskId) {
|
|
47
|
+
const paths = getTaskPaths(repoRoot, taskId);
|
|
48
|
+
return exists(paths.taskConfigPath);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Explicit Task↔DAG association only:
|
|
52
|
+
* - lifecycle.json recorded runAssociations
|
|
53
|
+
* - DagSpec.taskContractBinding.taskId
|
|
54
|
+
* - DagSpec.sourceBinding.taskId
|
|
55
|
+
* - state.workerAssociation.taskId
|
|
56
|
+
*
|
|
57
|
+
* Never uses taskId substring or full JSON string includes.
|
|
58
|
+
*/
|
|
59
|
+
export async function listExplicitlyAssociatedRuns(repoRoot, taskId) {
|
|
60
|
+
const record = await loadLifecycleRecord(repoRoot, taskId);
|
|
61
|
+
const recorded = new Map((record?.runAssociations ?? []).map((entry) => [entry.runId, entry.source]));
|
|
62
|
+
const entries = await listAllDagRunEntries(repoRoot);
|
|
63
|
+
const related = [];
|
|
64
|
+
for (const entry of entries) {
|
|
65
|
+
let source = recorded.get(entry.runId) ?? null;
|
|
66
|
+
let stateStatus;
|
|
67
|
+
let failureCategory;
|
|
68
|
+
let lastMeaningfulProgressAt;
|
|
69
|
+
try {
|
|
70
|
+
const state = await readDagRunState(entry.runDir);
|
|
71
|
+
stateStatus = state.status;
|
|
72
|
+
const liveness = assessDagRunLiveness({ state });
|
|
73
|
+
const activeNode = Object.values(state.nodes ?? {}).find((node) => node.status === "RUNNING");
|
|
74
|
+
lastMeaningfulProgressAt =
|
|
75
|
+
activeNode?.lastMeaningfulProgressAt ??
|
|
76
|
+
activeNode?.lastProviderActivityAt ??
|
|
77
|
+
activeNode?.lastToolActivityAt ??
|
|
78
|
+
activeNode?.lastOutputActivityAt ??
|
|
79
|
+
activeNode?.lastActivityAt ??
|
|
80
|
+
activeNode?.startedAt ??
|
|
81
|
+
null;
|
|
82
|
+
if (liveness.status === "orphaned" ||
|
|
83
|
+
liveness.status === "stale" ||
|
|
84
|
+
liveness.status === "needs-attention" ||
|
|
85
|
+
liveness.status === "suspected-stall") {
|
|
86
|
+
failureCategory =
|
|
87
|
+
state.failureCategory ??
|
|
88
|
+
(liveness.status === "orphaned"
|
|
89
|
+
? "orphaned"
|
|
90
|
+
: liveness.status === "stale"
|
|
91
|
+
? "stale-heartbeat"
|
|
92
|
+
: liveness.status);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
failureCategory = state.failureCategory ?? null;
|
|
96
|
+
}
|
|
97
|
+
const workerTaskId = state.workerAssociation?.taskId;
|
|
98
|
+
if (!source && workerTaskId === taskId) {
|
|
99
|
+
source = "worker-association";
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// state unreadable; may still match via binding
|
|
104
|
+
}
|
|
105
|
+
if (!source) {
|
|
106
|
+
try {
|
|
107
|
+
const spec = await readDagRunSpec(entry.runDir);
|
|
108
|
+
if (spec.taskContractBinding?.taskId === taskId) {
|
|
109
|
+
source = "task-contract-binding";
|
|
110
|
+
}
|
|
111
|
+
else if (spec.sourceBinding?.taskId === taskId) {
|
|
112
|
+
source = "source-binding";
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// ignore
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (!source)
|
|
120
|
+
continue;
|
|
121
|
+
related.push({
|
|
122
|
+
runId: entry.runId,
|
|
123
|
+
lifecycle: entry.lifecycle,
|
|
124
|
+
runDir: entry.runDir,
|
|
125
|
+
status: stateStatus,
|
|
126
|
+
failureCategory,
|
|
127
|
+
lastMeaningfulProgressAt,
|
|
128
|
+
source,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
return related.sort((a, b) => a.runId.localeCompare(b.runId));
|
|
132
|
+
}
|
|
133
|
+
function deriveLifecycleState(input) {
|
|
134
|
+
if (!input.exists)
|
|
135
|
+
return "absent";
|
|
136
|
+
if (input.needsAttention)
|
|
137
|
+
return "needs-attention";
|
|
138
|
+
if (input.awaitingDecision)
|
|
139
|
+
return "awaiting-decision";
|
|
140
|
+
if (input.activeRun)
|
|
141
|
+
return "running";
|
|
142
|
+
if (input.closeoutExists && input.promotionReady)
|
|
143
|
+
return "evidence-closed";
|
|
144
|
+
if (input.latestRun?.lifecycle === "completed") {
|
|
145
|
+
if (input.latestRun.status === "finished")
|
|
146
|
+
return "run-succeeded";
|
|
147
|
+
return "run-failed";
|
|
148
|
+
}
|
|
149
|
+
if (input.gateOpen)
|
|
150
|
+
return "awaiting-write-set-approval";
|
|
151
|
+
if (input.dagValidated)
|
|
152
|
+
return "dag-validated";
|
|
153
|
+
if (input.contractManaged && input.sourceReady)
|
|
154
|
+
return "contract-managed";
|
|
155
|
+
return "intake-incomplete";
|
|
156
|
+
}
|
|
157
|
+
export async function observeTaskLifecycle(repoRoot, taskId) {
|
|
158
|
+
const existsFlag = await taskExists(repoRoot, taskId);
|
|
159
|
+
if (!existsFlag) {
|
|
160
|
+
return {
|
|
161
|
+
schemaVersion: 1,
|
|
162
|
+
taskId,
|
|
163
|
+
exists: false,
|
|
164
|
+
lifecycleState: "absent",
|
|
165
|
+
runs: [],
|
|
166
|
+
completedTransitions: [],
|
|
167
|
+
blockers: [],
|
|
168
|
+
warnings: [],
|
|
169
|
+
next: {
|
|
170
|
+
kind: "advance",
|
|
171
|
+
command: `loop-agent task advance ${taskId} --prd <path> --allowed-path <glob> --json`,
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
const paths = getTaskPaths(repoRoot, taskId);
|
|
176
|
+
const taskDir = getTaskDir(repoRoot, taskId);
|
|
177
|
+
const record = await loadLifecycleRecord(repoRoot, taskId);
|
|
178
|
+
const contractState = await observeTaskContractState(repoRoot, taskId);
|
|
179
|
+
let title;
|
|
180
|
+
let taskConfigAllowed = [];
|
|
181
|
+
try {
|
|
182
|
+
const config = await loadTaskConfig(repoRoot, taskId);
|
|
183
|
+
title = config.title;
|
|
184
|
+
taskConfigAllowed = config.allowedPaths ?? [];
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
// keep optional
|
|
188
|
+
}
|
|
189
|
+
const requirementPath = path.join(paths.sourceDir, "需求.md");
|
|
190
|
+
const constraintsPath = path.join(paths.sourceDir, "执行约束.md");
|
|
191
|
+
const requirementReady = await exists(requirementPath);
|
|
192
|
+
const constraintsReady = await exists(constraintsPath);
|
|
193
|
+
const missing = [
|
|
194
|
+
...(requirementReady ? [] : [requirementPath]),
|
|
195
|
+
...(constraintsReady ? [] : [constraintsPath]),
|
|
196
|
+
];
|
|
197
|
+
let importedPrdCount = 0;
|
|
198
|
+
const referencesDir = path.join(paths.sourceDir, "references");
|
|
199
|
+
if (await exists(referencesDir)) {
|
|
200
|
+
// Count is best-effort; absence is fine.
|
|
201
|
+
try {
|
|
202
|
+
const { readdir } = await import("node:fs/promises");
|
|
203
|
+
const entries = await readdir(referencesDir);
|
|
204
|
+
importedPrdCount = entries.filter((name) => name.endsWith(".md")).length;
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
importedPrdCount = 0;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const dagDraftPath = paths.dagDraftPath;
|
|
211
|
+
const dagDraftExists = await exists(dagDraftPath);
|
|
212
|
+
let specHash = null;
|
|
213
|
+
let sourceBindingTaskId = null;
|
|
214
|
+
let taskContractBindingTaskId = null;
|
|
215
|
+
let writeSet = [];
|
|
216
|
+
let rawSpecText = null;
|
|
217
|
+
let parsedSpec = null;
|
|
218
|
+
if (dagDraftExists) {
|
|
219
|
+
try {
|
|
220
|
+
rawSpecText = await readFile(dagDraftPath, "utf-8");
|
|
221
|
+
specHash = hashDagSpecBytes(rawSpecText);
|
|
222
|
+
parsedSpec = JSON.parse(rawSpecText);
|
|
223
|
+
sourceBindingTaskId = parsedSpec.sourceBinding?.taskId ?? null;
|
|
224
|
+
taskContractBindingTaskId =
|
|
225
|
+
parsedSpec.taskContractBinding?.taskId ?? null;
|
|
226
|
+
writeSet = collectWriterWriteSet(parsedSpec);
|
|
227
|
+
if (writeSet.length === 0 && taskConfigAllowed.length > 0) {
|
|
228
|
+
writeSet = normalizeWriteSet(taskConfigAllowed);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
specHash = null;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
const runs = await listExplicitlyAssociatedRuns(repoRoot, taskId);
|
|
236
|
+
const activeRun = runs.find((run) => run.lifecycle === "active" || run.lifecycle === "paused") ?? null;
|
|
237
|
+
const latestRun = runs.length > 0 ? runs[runs.length - 1] : null;
|
|
238
|
+
const strictValidated = Boolean(record?.strictValidatedAt) &&
|
|
239
|
+
Boolean(record?.lastDagSpecHash) &&
|
|
240
|
+
record?.lastDagSpecHash === specHash;
|
|
241
|
+
const identity = resolveRunningControllerIdentity();
|
|
242
|
+
const gateBound = dagDraftExists && specHash
|
|
243
|
+
? buildGateBinding({
|
|
244
|
+
taskId,
|
|
245
|
+
contractRevision: logicalRevisionForState(contractState),
|
|
246
|
+
contractCanonicalHash: contractState.ref?.canonicalHash ??
|
|
247
|
+
contractState.observedCanonicalHash ??
|
|
248
|
+
null,
|
|
249
|
+
sourceBindingHash: parsedSpec ? hashSourceBinding(parsedSpec) : null,
|
|
250
|
+
dagSpecHash: specHash,
|
|
251
|
+
writeSet,
|
|
252
|
+
controllerIdentityAnchor: controllerIdentityAnchor(identity
|
|
253
|
+
? {
|
|
254
|
+
packageName: identity.packageName,
|
|
255
|
+
packageVersion: identity.packageVersion,
|
|
256
|
+
packageFingerprintValue: identity.packageFingerprint.value,
|
|
257
|
+
}
|
|
258
|
+
: null),
|
|
259
|
+
})
|
|
260
|
+
: null;
|
|
261
|
+
const lastGate = record?.lastGate;
|
|
262
|
+
const currentDigest = gateBound != null
|
|
263
|
+
? buildWriteSetGate({ taskId, bound: gateBound }).digest
|
|
264
|
+
: null;
|
|
265
|
+
const rejectedCurrent = lastGate?.status === "rejected" &&
|
|
266
|
+
Boolean(currentDigest) &&
|
|
267
|
+
lastGate.digest === currentDigest;
|
|
268
|
+
const approvedCurrent = lastGate?.status === "approved" &&
|
|
269
|
+
Boolean(currentDigest) &&
|
|
270
|
+
lastGate.digest === currentDigest;
|
|
271
|
+
// Open gate only when draft is strict-validated and the current binding is
|
|
272
|
+
// not already approved/rejected. Rejected same-digest gates stay closed
|
|
273
|
+
// until contract/source/DAG/writeSet/controller binding changes.
|
|
274
|
+
const gate = gateBound && strictValidated && !approvedCurrent && !rejectedCurrent
|
|
275
|
+
? buildWriteSetGate({ taskId, bound: gateBound })
|
|
276
|
+
: null;
|
|
277
|
+
const gateOpen = Boolean(gate);
|
|
278
|
+
let legacyState;
|
|
279
|
+
try {
|
|
280
|
+
legacyState = await getTaskStatus(repoRoot, taskId);
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
legacyState = null;
|
|
284
|
+
}
|
|
285
|
+
const promotionReady = Boolean(legacyState?.artifacts.modifyLog && legacyState?.artifacts.verifyResult);
|
|
286
|
+
const closeout = closeoutPath(repoRoot, taskId);
|
|
287
|
+
const closeoutExists = await exists(closeout);
|
|
288
|
+
const awaitingDecision = activeRun?.lifecycle === "paused" ||
|
|
289
|
+
activeRun?.status === "paused" ||
|
|
290
|
+
Boolean(activeRun?.status === "waiting-for-human");
|
|
291
|
+
const needsAttention = activeRun?.status === "error" ||
|
|
292
|
+
(activeRun?.lifecycle === "active" &&
|
|
293
|
+
(activeRun.failureCategory === "termination-unconfirmed" ||
|
|
294
|
+
activeRun.failureCategory === "orphaned" ||
|
|
295
|
+
activeRun.failureCategory === "stale-heartbeat" ||
|
|
296
|
+
activeRun.failureCategory === "needs-attention" ||
|
|
297
|
+
activeRun.failureCategory === "suspected-stall" ||
|
|
298
|
+
activeRun.failureCategory === "monitor-timeout"));
|
|
299
|
+
const contractManaged = contractState.effectiveStatus === "managed" || Boolean(contractState.ref);
|
|
300
|
+
const lifecycleState = deriveLifecycleState({
|
|
301
|
+
exists: true,
|
|
302
|
+
contractManaged,
|
|
303
|
+
sourceReady: requirementReady,
|
|
304
|
+
dagValidated: strictValidated,
|
|
305
|
+
gateOpen: Boolean(gate),
|
|
306
|
+
activeRun,
|
|
307
|
+
latestRun,
|
|
308
|
+
promotionReady,
|
|
309
|
+
closeoutExists: closeoutExists || Boolean(record?.closeout),
|
|
310
|
+
needsAttention: Boolean(needsAttention),
|
|
311
|
+
awaitingDecision: Boolean(awaitingDecision),
|
|
312
|
+
});
|
|
313
|
+
const next = deriveNextFromSnapshot({
|
|
314
|
+
taskId,
|
|
315
|
+
lifecycleState,
|
|
316
|
+
gate,
|
|
317
|
+
});
|
|
318
|
+
return {
|
|
319
|
+
schemaVersion: 1,
|
|
320
|
+
taskId,
|
|
321
|
+
exists: true,
|
|
322
|
+
lifecycleState,
|
|
323
|
+
title,
|
|
324
|
+
taskDir,
|
|
325
|
+
contract: {
|
|
326
|
+
effectiveStatus: contractState.effectiveStatus,
|
|
327
|
+
revision: logicalRevisionForState(contractState),
|
|
328
|
+
canonicalHash: contractState.ref?.canonicalHash ?? null,
|
|
329
|
+
observedCanonicalHash: contractState.observedCanonicalHash ?? null,
|
|
330
|
+
transactionIncomplete: contractState.effectiveStatus === "transaction-incomplete",
|
|
331
|
+
taskConfigSha256: contractState.ref?.taskConfigSha256 ?? null,
|
|
332
|
+
projectionVersion: contractState.ref?.projectionVersion ?? null,
|
|
333
|
+
canonicalizerVersion: contractState.ref?.canonicalizerVersion ?? null,
|
|
334
|
+
taskConfigSchemaVersion: contractState.ref?.taskConfigSchemaVersion ?? null,
|
|
335
|
+
},
|
|
336
|
+
source: {
|
|
337
|
+
requirementReady,
|
|
338
|
+
constraintsReady,
|
|
339
|
+
importedPrdCount,
|
|
340
|
+
missing,
|
|
341
|
+
},
|
|
342
|
+
dagDraft: {
|
|
343
|
+
path: dagDraftPath,
|
|
344
|
+
exists: dagDraftExists,
|
|
345
|
+
specHash,
|
|
346
|
+
sourceBindingTaskId,
|
|
347
|
+
taskContractBindingTaskId,
|
|
348
|
+
writeSet,
|
|
349
|
+
strictValidated,
|
|
350
|
+
},
|
|
351
|
+
gate,
|
|
352
|
+
activeRun,
|
|
353
|
+
latestRun,
|
|
354
|
+
runs,
|
|
355
|
+
promotion: {
|
|
356
|
+
required: Boolean(latestRun && latestRun.lifecycle === "completed"),
|
|
357
|
+
ready: promotionReady || Boolean(record?.promotion),
|
|
358
|
+
},
|
|
359
|
+
closeout: {
|
|
360
|
+
path: closeout,
|
|
361
|
+
exists: closeoutExists || Boolean(record?.closeout),
|
|
362
|
+
},
|
|
363
|
+
completedTransitions: record?.completedTransitions ?? [],
|
|
364
|
+
blockers: [],
|
|
365
|
+
warnings: [],
|
|
366
|
+
next,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
function deriveNextFromSnapshot(input) {
|
|
370
|
+
if (input.lifecycleState === "evidence-closed") {
|
|
371
|
+
return { kind: "none" };
|
|
372
|
+
}
|
|
373
|
+
if (input.gate && input.lifecycleState === "awaiting-write-set-approval") {
|
|
374
|
+
return {
|
|
375
|
+
kind: "approve-gate",
|
|
376
|
+
command: input.gate.approveCommand,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
if (input.lifecycleState === "running") {
|
|
380
|
+
return {
|
|
381
|
+
kind: "status",
|
|
382
|
+
command: `loop-agent task status ${input.taskId} --json`,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
return {
|
|
386
|
+
kind: "advance",
|
|
387
|
+
command: `loop-agent task advance ${input.taskId} --json`,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
export function hashDagSpecFileContent(content) {
|
|
391
|
+
return hashDagSpecBytes(content);
|
|
392
|
+
}
|
|
393
|
+
export function collectNormalizedWriteSetFromSpec(spec) {
|
|
394
|
+
return collectWriterWriteSet(spec);
|
|
395
|
+
}
|