@tea-agent/loop-agent 0.16.1-beta.2 → 0.16.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.
- package/AGENTS.md +4 -8
- package/CHANGELOG.md +76 -18
- package/README.md +76 -299
- package/dist/application/evaluation/alias.js +184 -0
- package/dist/application/evaluation/budget.js +192 -0
- package/dist/application/evaluation/campaign-hash.js +47 -0
- package/dist/application/evaluation/campaign-matrix.js +372 -0
- package/dist/application/evaluation/campaign-scorecard.js +135 -0
- package/dist/application/evaluation/campaign.js +370 -0
- package/dist/application/evaluation/candidate.js +23 -6
- package/dist/application/evaluation/corpus-hash.js +38 -0
- package/dist/application/evaluation/corpus.js +56 -0
- package/dist/application/evaluation/experiment.js +294 -0
- package/dist/application/evaluation/ignition.js +198 -0
- package/dist/application/evaluation/integrity-audit.js +162 -0
- package/dist/application/evaluation/outer-loop.js +132 -0
- package/dist/application/evaluation/pi-cell-executor.js +39 -0
- package/dist/application/evaluation/private-verifier.js +46 -0
- package/dist/application/evaluation/promotion-policy.js +151 -0
- package/dist/application/evaluation/proposer.js +98 -0
- package/dist/application/evaluation/types.js +522 -0
- package/dist/cli/command-definitions.js +19 -3
- package/dist/commands/dag-reconcile-run.js +3 -116
- package/dist/commands/eval.js +1176 -13
- package/dist/commands/init.js +7 -1
- package/dist/executors/dag-pi-executor.js +8 -46
- package/dist/executors/pi-sdk-executor.js +66 -3
- package/dist/executors/shell-executor.js +213 -30
- package/dist/executors/shell-presets.js +12 -2
- package/dist/executors/shell-write-guard.js +20 -1
- package/dist/infrastructure/evaluation/alias-store.js +199 -0
- package/dist/infrastructure/evaluation/campaign-store.js +154 -0
- package/dist/infrastructure/evaluation/corpus-store.js +181 -0
- package/dist/infrastructure/evaluation/experiment-store.js +124 -0
- package/dist/infrastructure/evaluation/ignition-store.js +82 -0
- package/dist/infrastructure/evaluation/private-verifier-store.js +145 -0
- package/dist/infrastructure/evaluation/proposer-store.js +78 -0
- package/dist/records/promotion.js +3 -1
- package/dist/shared/git-progress.js +9 -2
- package/dist/worker/cli.js +83 -0
- package/dist/worker/delivery/git-transaction.js +75 -0
- package/dist/worker/delivery/verification-bundle.js +13 -2
- package/dist/worker/feature/review.js +3 -2
- package/dist/worker/observability/read-model.js +56 -0
- package/dist/worker/observe/server.js +6 -3
- package/dist/worker/observe/static/dag-helpers.js +0 -62
- package/dist/worker/observe/static/styles.css +18 -55
- package/dist/worker/observe/static/views/dag.js +13 -5
- package/dist/worker/outcomes/adapters.js +4 -1
- package/dist/worker/outcomes/declared-artifacts.js +103 -0
- package/dist/worker/outcomes/evidence-tokens.js +29 -0
- package/dist/worker/outcomes/gate.js +10 -11
- package/dist/worker/outcomes/projector.js +30 -4
- package/dist/worker/outcomes/types.js +3 -0
- package/dist/worker/pool/reconcile.js +285 -0
- package/dist/worker/run-task/run-task.js +81 -4
- package/dist/worker/runner/run-ready.js +25 -2
- package/dist/worker/task-graph/ready-planner.js +14 -8
- package/dist/worker/task-graph/task-graph-schema.js +5 -3
- package/dist/workflows/dag/backend-test-analysis-contract.js +87 -30
- package/dist/workflows/dag/backend-test-case-manifest.js +71 -8
- package/dist/workflows/dag/backend-test-execution-contract.js +63 -11
- package/dist/workflows/dag/backend-test-repair-contract.js +94 -0
- package/dist/workflows/dag/backend-test-result-contract.js +6 -4
- package/dist/workflows/dag/backend-test-semantic-review-contract.js +36 -0
- package/dist/workflows/dag/budget-enforcement.js +67 -0
- package/dist/workflows/dag/context-policy.js +137 -0
- package/dist/workflows/dag/dynamic-runtime/condition.js +1 -1
- package/dist/workflows/dag/dynamic-runtime/shared.js +42 -0
- package/dist/workflows/dag/failure-routing.js +8 -1
- package/dist/workflows/dag/frontend-implementation-contract.js +32 -93
- package/dist/workflows/dag/init-hybrid.js +624 -172
- package/dist/workflows/dag/knowledge-curator.js +3 -0
- package/dist/workflows/dag/lifecycle.js +33 -2
- package/dist/workflows/dag/node-execution.js +11 -4
- package/dist/workflows/dag/prompt.js +1 -1
- package/dist/workflows/dag/reconcile-run.js +121 -0
- package/dist/workflows/dag/report.js +12 -0
- package/dist/workflows/dag/runner.js +43 -16
- package/dist/workflows/dag/scheduler.js +87 -17
- package/dist/workflows/dag/skill-snapshot.js +11 -7
- package/dist/workflows/dag/types.js +49 -1
- package/dist/workflows/dag/validate.js +35 -15
- package/docs/README.md +3 -1
- package/docs/architecture/runtime-boundaries.md +3 -2
- package/docs/init-surface.manifest.json +4 -0
- package/docs/local-development-environment.md +52 -0
- package/docs/templates/agent-dag.schema.json +25 -7
- package/docs/templates/agent-dag.supervised-implementation.json +23 -4
- package/docs/templates/backend-test-analysis.schema.json +9 -16
- package/docs/templates/backend-test-dag.json +493 -197
- package/docs/templates/backend-test-dag.review-cases.prompt.md +10 -4
- package/docs/templates/backend-test-execution.schema.json +6 -1
- package/docs/templates/branch-merge-report.md +14 -0
- package/docs/templates/evaluation/campaign-budget-v1.json +12 -0
- package/docs/templates/evaluation/campaign-dogfood-v0.json +24 -0
- package/docs/templates/evaluation/campaign-evidence-v1.json +44 -0
- package/docs/templates/evaluation/context-policy-baseline-v1.json +17 -0
- package/docs/templates/evaluation/context-policy-role-specialized-v1.json +28 -0
- package/docs/templates/evaluation/corpus-dogfood-v0.manifest.json +118 -0
- package/docs/templates/evaluation/matrix-dag-dry-run-v1.json +21 -0
- package/docs/templates/evaluation/matrix-fixture-v1.json +10 -0
- package/docs/templates/evaluation/private-verifier-dogfood-v0.json +16 -0
- package/docs/templates/product-line/AGENTS.md +1 -0
- package/docs/templates/product-line/README.md +17 -0
- package/docs/templates/product-line/acceptance.yaml +9 -0
- package/docs/templates/product-line/feature.yaml +11 -0
- package/docs/templates/product-line/task-graph.yaml +8 -0
- package/docs/templates/product-line/task.yaml +4 -0
- package/package.json +2 -1
- package/skills/frontend-implementation/references/node-contracts.md +3 -3
- package/skills/loop-agent/references/command-reference.md +5 -0
- package/skills/loop-agent/references/hybrid-dag.md +7 -4
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { access, mkdir, mkdtemp, readdir, readFile, rename, rm } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { ignitionIdSchema, ignitionReportSchema, } from "../../application/evaluation/types.js";
|
|
4
|
+
import { sha256Hex } from "../../application/evaluation/candidate-hash.js";
|
|
5
|
+
import { writeJsonAtomic, writeTextAtomic } from "../harness/atomic-write.js";
|
|
6
|
+
import { EVALUATION_ROOT } from "./store.js";
|
|
7
|
+
const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
8
|
+
const MANIFEST_INTEGRITY_FILE = "manifest.sha256";
|
|
9
|
+
export function assertIgnitionId(value) {
|
|
10
|
+
ignitionIdSchema.parse(value);
|
|
11
|
+
}
|
|
12
|
+
export function ignitionDir(repoRoot, ignitionId) {
|
|
13
|
+
assertIgnitionId(ignitionId);
|
|
14
|
+
return path.join(repoRoot, EVALUATION_ROOT, "ignition", ignitionId);
|
|
15
|
+
}
|
|
16
|
+
export function ignitionReportPath(repoRoot, ignitionId) {
|
|
17
|
+
return path.join(ignitionDir(repoRoot, ignitionId), "report.json");
|
|
18
|
+
}
|
|
19
|
+
async function pathExists(filePath) {
|
|
20
|
+
try {
|
|
21
|
+
await access(filePath);
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function serialize(report) {
|
|
29
|
+
return `${JSON.stringify(report, null, 2)}\n`;
|
|
30
|
+
}
|
|
31
|
+
export async function readIgnitionReport(repoRoot, ignitionId) {
|
|
32
|
+
assertIgnitionId(ignitionId);
|
|
33
|
+
const reportPath = ignitionReportPath(repoRoot, ignitionId);
|
|
34
|
+
if (!(await pathExists(reportPath)))
|
|
35
|
+
return undefined;
|
|
36
|
+
const rawText = await readFile(reportPath, "utf-8");
|
|
37
|
+
const expected = (await readFile(path.join(ignitionDir(repoRoot, ignitionId), MANIFEST_INTEGRITY_FILE), "utf-8")).trim();
|
|
38
|
+
if (sha256Hex(rawText) !== expected) {
|
|
39
|
+
throw new Error(`ignition report integrity mismatch for ${ignitionId}`);
|
|
40
|
+
}
|
|
41
|
+
return ignitionReportSchema.parse(JSON.parse(rawText));
|
|
42
|
+
}
|
|
43
|
+
export async function writeIgnitionReport(input) {
|
|
44
|
+
const parsed = ignitionReportSchema.parse(input.report);
|
|
45
|
+
const reportPath = ignitionReportPath(input.repoRoot, parsed.ignitionId);
|
|
46
|
+
const root = path.dirname(ignitionDir(input.repoRoot, parsed.ignitionId));
|
|
47
|
+
await mkdir(root, { recursive: true });
|
|
48
|
+
if (await pathExists(reportPath)) {
|
|
49
|
+
await writeJsonAtomic(reportPath, parsed, { repoRoot: input.repoRoot });
|
|
50
|
+
await writeTextAtomic(path.join(ignitionDir(input.repoRoot, parsed.ignitionId), MANIFEST_INTEGRITY_FILE), `${sha256Hex(serialize(parsed))}\n`, { repoRoot: input.repoRoot });
|
|
51
|
+
return reportPath;
|
|
52
|
+
}
|
|
53
|
+
const stagingDir = await mkdtemp(path.join(root, `.${parsed.ignitionId}.register-`));
|
|
54
|
+
try {
|
|
55
|
+
await writeJsonAtomic(path.join(stagingDir, "report.json"), parsed, {
|
|
56
|
+
repoRoot: input.repoRoot,
|
|
57
|
+
});
|
|
58
|
+
await writeTextAtomic(path.join(stagingDir, MANIFEST_INTEGRITY_FILE), `${sha256Hex(serialize(parsed))}\n`, { repoRoot: input.repoRoot });
|
|
59
|
+
await rename(stagingDir, ignitionDir(input.repoRoot, parsed.ignitionId));
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
await rm(stagingDir, { recursive: true, force: true });
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
return reportPath;
|
|
66
|
+
}
|
|
67
|
+
export async function listIgnitionIds(repoRoot) {
|
|
68
|
+
const root = path.join(repoRoot, EVALUATION_ROOT, "ignition");
|
|
69
|
+
try {
|
|
70
|
+
const entries = await readdir(root, { withFileTypes: true });
|
|
71
|
+
return entries
|
|
72
|
+
.filter((entry) => entry.isDirectory() && SAFE_ID.test(entry.name))
|
|
73
|
+
.map((entry) => entry.name)
|
|
74
|
+
.sort();
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
const code = error.code;
|
|
78
|
+
if (code === "ENOENT")
|
|
79
|
+
return [];
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { access, mkdir, mkdtemp, readdir, readFile, rename, rm } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { assertPrivateVerifierHash, computePrivateVerifierManifestHash, } from "../../application/evaluation/campaign-hash.js";
|
|
4
|
+
import { formatContentSha, normalizeContentSha, sha256Hex, } from "../../application/evaluation/candidate-hash.js";
|
|
5
|
+
import { privateVerifierIdSchema, privateVerifierManifestInputSchema, privateVerifierManifestSchema, } from "../../application/evaluation/types.js";
|
|
6
|
+
import { writeJsonAtomic, writeTextAtomic } from "../harness/atomic-write.js";
|
|
7
|
+
import { EVALUATION_ROOT } from "./store.js";
|
|
8
|
+
const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
9
|
+
const MANIFEST_INTEGRITY_FILE = "manifest.sha256";
|
|
10
|
+
export function assertPrivateVerifierId(value) {
|
|
11
|
+
privateVerifierIdSchema.parse(value);
|
|
12
|
+
}
|
|
13
|
+
export function privateVerifierDir(repoRoot, privateVerifierId) {
|
|
14
|
+
assertPrivateVerifierId(privateVerifierId);
|
|
15
|
+
return path.join(repoRoot, EVALUATION_ROOT, "private-verifiers", privateVerifierId);
|
|
16
|
+
}
|
|
17
|
+
export function privateVerifierManifestPath(repoRoot, privateVerifierId) {
|
|
18
|
+
return path.join(privateVerifierDir(repoRoot, privateVerifierId), "manifest.json");
|
|
19
|
+
}
|
|
20
|
+
async function pathExists(filePath) {
|
|
21
|
+
try {
|
|
22
|
+
await access(filePath);
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function serializeManifest(manifest) {
|
|
30
|
+
return `${JSON.stringify(manifest, null, 2)}\n`;
|
|
31
|
+
}
|
|
32
|
+
export async function materializePrivateVerifierManifest(raw) {
|
|
33
|
+
const input = privateVerifierManifestInputSchema.parse(raw);
|
|
34
|
+
const withoutHash = {
|
|
35
|
+
schemaVersion: 1,
|
|
36
|
+
privateVerifierId: input.privateVerifierId,
|
|
37
|
+
createdAt: input.createdAt,
|
|
38
|
+
...(input.description !== undefined
|
|
39
|
+
? { description: input.description }
|
|
40
|
+
: {}),
|
|
41
|
+
checks: input.checks,
|
|
42
|
+
};
|
|
43
|
+
const manifestHash = computePrivateVerifierManifestHash(withoutHash);
|
|
44
|
+
if (input.manifestHash) {
|
|
45
|
+
const provided = formatContentSha(normalizeContentSha(input.manifestHash));
|
|
46
|
+
if (provided !== manifestHash) {
|
|
47
|
+
throw new Error(`privateVerifier manifestHash mismatch: expected ${manifestHash}, got ${provided}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return privateVerifierManifestSchema.parse({
|
|
51
|
+
...withoutHash,
|
|
52
|
+
manifestHash,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
export async function loadPrivateVerifierManifestFromPath(repoRoot, manifestPath) {
|
|
56
|
+
const resolved = path.isAbsolute(manifestPath)
|
|
57
|
+
? manifestPath
|
|
58
|
+
: path.resolve(repoRoot, manifestPath);
|
|
59
|
+
const raw = JSON.parse(await readFile(resolved, "utf-8"));
|
|
60
|
+
return privateVerifierManifestInputSchema.parse(raw);
|
|
61
|
+
}
|
|
62
|
+
export async function readPrivateVerifierManifest(repoRoot, privateVerifierId) {
|
|
63
|
+
assertPrivateVerifierId(privateVerifierId);
|
|
64
|
+
const manifestPath = privateVerifierManifestPath(repoRoot, privateVerifierId);
|
|
65
|
+
const rawText = await readFile(manifestPath, "utf-8");
|
|
66
|
+
const integrityPath = path.join(privateVerifierDir(repoRoot, privateVerifierId), MANIFEST_INTEGRITY_FILE);
|
|
67
|
+
const expected = (await readFile(integrityPath, "utf-8")).trim();
|
|
68
|
+
const actual = sha256Hex(rawText);
|
|
69
|
+
if (expected !== actual) {
|
|
70
|
+
throw new Error(`privateVerifier manifest integrity mismatch for ${privateVerifierId}`);
|
|
71
|
+
}
|
|
72
|
+
const stored = privateVerifierManifestSchema.parse(JSON.parse(rawText));
|
|
73
|
+
if (stored.privateVerifierId !== privateVerifierId) {
|
|
74
|
+
throw new Error(`privateVerifier identity mismatch: directory=${privateVerifierId}, manifest=${stored.privateVerifierId}`);
|
|
75
|
+
}
|
|
76
|
+
assertPrivateVerifierHash(stored);
|
|
77
|
+
return stored;
|
|
78
|
+
}
|
|
79
|
+
export async function listPrivateVerifierIds(repoRoot) {
|
|
80
|
+
const root = path.join(repoRoot, EVALUATION_ROOT, "private-verifiers");
|
|
81
|
+
try {
|
|
82
|
+
const entries = await readdir(root, { withFileTypes: true });
|
|
83
|
+
return entries
|
|
84
|
+
.filter((entry) => entry.isDirectory() && SAFE_ID.test(entry.name))
|
|
85
|
+
.map((entry) => entry.name)
|
|
86
|
+
.sort();
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
const code = error.code;
|
|
90
|
+
if (code === "ENOENT")
|
|
91
|
+
return [];
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function manifestsEqual(a, b) {
|
|
96
|
+
return (a.privateVerifierId === b.privateVerifierId &&
|
|
97
|
+
a.manifestHash === b.manifestHash &&
|
|
98
|
+
a.createdAt === b.createdAt &&
|
|
99
|
+
(a.description ?? "") === (b.description ?? "") &&
|
|
100
|
+
JSON.stringify(a.checks) === JSON.stringify(b.checks));
|
|
101
|
+
}
|
|
102
|
+
export async function registerPrivateVerifierManifest(input) {
|
|
103
|
+
const { repoRoot, manifest } = input;
|
|
104
|
+
assertPrivateVerifierId(manifest.privateVerifierId);
|
|
105
|
+
const manifestPath = privateVerifierManifestPath(repoRoot, manifest.privateVerifierId);
|
|
106
|
+
if (await pathExists(manifestPath)) {
|
|
107
|
+
const existing = await readPrivateVerifierManifest(repoRoot, manifest.privateVerifierId);
|
|
108
|
+
if (!manifestsEqual(existing, manifest)) {
|
|
109
|
+
throw new Error(`privateVerifier already exists with different content: ${manifest.privateVerifierId}`);
|
|
110
|
+
}
|
|
111
|
+
return { manifest: existing, idempotent: true, manifestPath };
|
|
112
|
+
}
|
|
113
|
+
const root = path.dirname(privateVerifierDir(repoRoot, manifest.privateVerifierId));
|
|
114
|
+
await mkdir(root, { recursive: true });
|
|
115
|
+
const stagingDir = await mkdtemp(path.join(root, `.${manifest.privateVerifierId}.register-`));
|
|
116
|
+
try {
|
|
117
|
+
await writeJsonAtomic(path.join(stagingDir, "manifest.json"), manifest, {
|
|
118
|
+
repoRoot,
|
|
119
|
+
});
|
|
120
|
+
await writeTextAtomic(path.join(stagingDir, MANIFEST_INTEGRITY_FILE), `${sha256Hex(serializeManifest(manifest))}\n`, { repoRoot });
|
|
121
|
+
try {
|
|
122
|
+
await rename(stagingDir, privateVerifierDir(repoRoot, manifest.privateVerifierId));
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
const code = error.code;
|
|
126
|
+
const isCreateRace = code === "EEXIST" ||
|
|
127
|
+
code === "ENOTEMPTY" ||
|
|
128
|
+
(code === "EPERM" &&
|
|
129
|
+
(await pathExists(privateVerifierDir(repoRoot, manifest.privateVerifierId))));
|
|
130
|
+
if (!isCreateRace)
|
|
131
|
+
throw error;
|
|
132
|
+
await rm(stagingDir, { recursive: true, force: true });
|
|
133
|
+
const existing = await readPrivateVerifierManifest(repoRoot, manifest.privateVerifierId);
|
|
134
|
+
if (!manifestsEqual(existing, manifest)) {
|
|
135
|
+
throw new Error(`privateVerifier already exists with different content: ${manifest.privateVerifierId}`);
|
|
136
|
+
}
|
|
137
|
+
return { manifest: existing, idempotent: true, manifestPath };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
await rm(stagingDir, { recursive: true, force: true });
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
return { manifest, idempotent: false, manifestPath };
|
|
145
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { access, mkdir, mkdtemp, readdir, readFile, rename, rm } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { proposerRunIdSchema, proposerRunSchema, } from "../../application/evaluation/types.js";
|
|
4
|
+
import { sha256Hex } from "../../application/evaluation/candidate-hash.js";
|
|
5
|
+
import { writeJsonAtomic, writeTextAtomic } from "../harness/atomic-write.js";
|
|
6
|
+
import { EVALUATION_ROOT } from "./store.js";
|
|
7
|
+
const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
8
|
+
const MANIFEST_INTEGRITY_FILE = "manifest.sha256";
|
|
9
|
+
export function assertProposerRunId(value) {
|
|
10
|
+
proposerRunIdSchema.parse(value);
|
|
11
|
+
}
|
|
12
|
+
export function proposerRunDir(repoRoot, proposerRunId) {
|
|
13
|
+
assertProposerRunId(proposerRunId);
|
|
14
|
+
return path.join(repoRoot, EVALUATION_ROOT, "proposer-runs", proposerRunId);
|
|
15
|
+
}
|
|
16
|
+
export function proposerRunManifestPath(repoRoot, proposerRunId) {
|
|
17
|
+
return path.join(proposerRunDir(repoRoot, proposerRunId), "manifest.json");
|
|
18
|
+
}
|
|
19
|
+
async function pathExists(filePath) {
|
|
20
|
+
try {
|
|
21
|
+
await access(filePath);
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function serialize(record) {
|
|
29
|
+
return `${JSON.stringify(record, null, 2)}\n`;
|
|
30
|
+
}
|
|
31
|
+
export async function writeProposerRun(input) {
|
|
32
|
+
const parsed = proposerRunSchema.parse(input.run);
|
|
33
|
+
const root = path.dirname(proposerRunDir(input.repoRoot, parsed.proposerRunId));
|
|
34
|
+
await mkdir(root, { recursive: true });
|
|
35
|
+
const stagingDir = await mkdtemp(path.join(root, `.${parsed.proposerRunId}.register-`));
|
|
36
|
+
const manifestPath = proposerRunManifestPath(input.repoRoot, parsed.proposerRunId);
|
|
37
|
+
try {
|
|
38
|
+
if (await pathExists(manifestPath)) {
|
|
39
|
+
throw new Error(`proposer run already exists: ${parsed.proposerRunId}`);
|
|
40
|
+
}
|
|
41
|
+
await writeJsonAtomic(path.join(stagingDir, "manifest.json"), parsed, {
|
|
42
|
+
repoRoot: input.repoRoot,
|
|
43
|
+
});
|
|
44
|
+
await writeTextAtomic(path.join(stagingDir, MANIFEST_INTEGRITY_FILE), `${sha256Hex(serialize(parsed))}\n`, { repoRoot: input.repoRoot });
|
|
45
|
+
await rename(stagingDir, proposerRunDir(input.repoRoot, parsed.proposerRunId));
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
await rm(stagingDir, { recursive: true, force: true });
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
return manifestPath;
|
|
52
|
+
}
|
|
53
|
+
export async function readProposerRun(repoRoot, proposerRunId) {
|
|
54
|
+
assertProposerRunId(proposerRunId);
|
|
55
|
+
const manifestPath = proposerRunManifestPath(repoRoot, proposerRunId);
|
|
56
|
+
const rawText = await readFile(manifestPath, "utf-8");
|
|
57
|
+
const expected = (await readFile(path.join(proposerRunDir(repoRoot, proposerRunId), MANIFEST_INTEGRITY_FILE), "utf-8")).trim();
|
|
58
|
+
if (sha256Hex(rawText) !== expected) {
|
|
59
|
+
throw new Error(`proposer run integrity mismatch for ${proposerRunId}`);
|
|
60
|
+
}
|
|
61
|
+
return proposerRunSchema.parse(JSON.parse(rawText));
|
|
62
|
+
}
|
|
63
|
+
export async function listProposerRunIds(repoRoot) {
|
|
64
|
+
const root = path.join(repoRoot, EVALUATION_ROOT, "proposer-runs");
|
|
65
|
+
try {
|
|
66
|
+
const entries = await readdir(root, { withFileTypes: true });
|
|
67
|
+
return entries
|
|
68
|
+
.filter((entry) => entry.isDirectory() && SAFE_ID.test(entry.name))
|
|
69
|
+
.map((entry) => entry.name)
|
|
70
|
+
.sort();
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
const code = error.code;
|
|
74
|
+
if (code === "ENOENT")
|
|
75
|
+
return [];
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -23,7 +23,9 @@ export async function closeoutTask(repoRoot, taskId) {
|
|
|
23
23
|
const verify = await readFile(verifyPath, 'utf-8').catch(() => '');
|
|
24
24
|
const slug = taskId.replace(/^\d{4}-\d{2}-\d{2}-/, '');
|
|
25
25
|
const datePrefix = taskId.match(/^\d{4}-\d{2}-\d{2}/)?.[0] ?? new Date().toISOString().slice(0, 10);
|
|
26
|
-
|
|
26
|
+
// Runtime bridge evidence stays under .harness/ so promote/closeout never
|
|
27
|
+
// invents tracked docs/progress writes outside TaskSpec allowed_paths.
|
|
28
|
+
const progressDir = path.join(repoRoot, '.harness', 'task-pool', 'artifacts', 'closeout');
|
|
27
29
|
await mkdir(progressDir, { recursive: true });
|
|
28
30
|
const progressPath = path.join(progressDir, `${datePrefix}-${slug}.md`);
|
|
29
31
|
const verificationSummary = summarizeVerification(verify);
|
|
@@ -114,8 +114,15 @@ function globToRegExp(pattern) {
|
|
|
114
114
|
const char = normalized[i];
|
|
115
115
|
const next = normalized[i + 1];
|
|
116
116
|
if (char === '*' && next === '*') {
|
|
117
|
-
|
|
118
|
-
|
|
117
|
+
const afterGlobstar = normalized[i + 2];
|
|
118
|
+
if (afterGlobstar === '/') {
|
|
119
|
+
regex += '(?:.*/)?';
|
|
120
|
+
i += 2;
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
regex += '.*';
|
|
124
|
+
i += 1;
|
|
125
|
+
}
|
|
119
126
|
continue;
|
|
120
127
|
}
|
|
121
128
|
if (char === '*') {
|
package/dist/worker/cli.js
CHANGED
|
@@ -16,6 +16,7 @@ import { buildGlobalSnapshot } from "./observability/read-model.js";
|
|
|
16
16
|
import { createObserveServer } from "./observe/server.js";
|
|
17
17
|
import { diagnoseTaskPoolStates, formatDoctorHuman, loadOperatorMapping } from "./pool/doctor.js";
|
|
18
18
|
import { migrateTaskPoolStates } from "./pool/migrate-state.js";
|
|
19
|
+
import { markTaskPoolFailed, reconcileTaskPoolAbandon } from "./pool/reconcile.js";
|
|
19
20
|
import { getTaskPoolRoot, prepareTaskPoolRetry, readFeatureTaskPoolStates } from "./pool/run-store.js";
|
|
20
21
|
import { TaskPoolStateError } from "./pool/types.js";
|
|
21
22
|
import { taskSpecSchema } from "./task-spec/schema.js";
|
|
@@ -32,6 +33,7 @@ import { previewFeatureCloseout, renderCloseoutPreview } from "./closeout/previe
|
|
|
32
33
|
import { applyFeatureCloseout } from "./closeout/apply.js";
|
|
33
34
|
import { projectMonthlyMetrics } from "./metrics/projector.js";
|
|
34
35
|
import { runFeatureFinalVerification } from "./delivery/final-verification.js";
|
|
36
|
+
import { advanceGitCheckpoint } from "./delivery/git-transaction.js";
|
|
35
37
|
export function buildAgentWorkerProgram() {
|
|
36
38
|
const program = new Command();
|
|
37
39
|
program
|
|
@@ -46,6 +48,29 @@ export function buildAgentWorkerProgram() {
|
|
|
46
48
|
const observe = program.command("observe").description("Observe UI server and snapshot utilities");
|
|
47
49
|
const feature = program.command("feature").description("Feature-level review and delivery workflow");
|
|
48
50
|
const pool = program.command("pool").description("Task Pool state doctor and migration utilities");
|
|
51
|
+
pool
|
|
52
|
+
.command("mark-failed")
|
|
53
|
+
.requiredOption("--repo <repo-root>", "Target repo root")
|
|
54
|
+
.requiredOption("--feature-id <feature-id>", "Canonical Feature id")
|
|
55
|
+
.requiredOption("--task-id <task-id>", "Task id")
|
|
56
|
+
.requiredOption("--worker-run-id <id>", "Worker run id to mark failed")
|
|
57
|
+
.requiredOption("--reason <reason>", "Why the emergency projection is justified")
|
|
58
|
+
.description("Audit-only emergency Task Pool Failed projection (does not mutate DAG lifecycle)")
|
|
59
|
+
.action(async (options) => {
|
|
60
|
+
try {
|
|
61
|
+
const result = await markTaskPoolFailed({
|
|
62
|
+
repoRoot: path.resolve(options.repo),
|
|
63
|
+
featureId: options.featureId,
|
|
64
|
+
taskId: options.taskId,
|
|
65
|
+
workerRunId: options.workerRunId,
|
|
66
|
+
reason: options.reason,
|
|
67
|
+
});
|
|
68
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
handlePoolRecoveryCliError(error);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
49
74
|
pool
|
|
50
75
|
.command("doctor")
|
|
51
76
|
.requiredOption("--repo <repo-root>", "Target repo root")
|
|
@@ -97,6 +122,28 @@ export function buildAgentWorkerProgram() {
|
|
|
97
122
|
process.exitCode = 1;
|
|
98
123
|
}
|
|
99
124
|
});
|
|
125
|
+
const featureGit = feature
|
|
126
|
+
.command("git")
|
|
127
|
+
.description("Feature Git transaction utilities");
|
|
128
|
+
featureGit
|
|
129
|
+
.command("advance-checkpoint")
|
|
130
|
+
.requiredOption("--repo <repo-root>", "Target repo root")
|
|
131
|
+
.requiredOption("--feature-id <feature-id>", "Canonical Feature id")
|
|
132
|
+
.requiredOption("--reason <reason>", "Why the manual revision is safe to record")
|
|
133
|
+
.description("Advance Git transaction lastCheckpoint to current HEAD on a clean owned branch")
|
|
134
|
+
.action(async (options) => {
|
|
135
|
+
try {
|
|
136
|
+
const result = await advanceGitCheckpoint({
|
|
137
|
+
repoRoot: path.resolve(options.repo),
|
|
138
|
+
featureId: options.featureId,
|
|
139
|
+
reason: options.reason,
|
|
140
|
+
});
|
|
141
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
handlePoolRecoveryCliError(error);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
100
147
|
feature
|
|
101
148
|
.command("review")
|
|
102
149
|
.requiredOption("--feature-dir <dir>", "Feature directory containing acceptance.yaml and tasks/")
|
|
@@ -262,6 +309,37 @@ export function buildAgentWorkerProgram() {
|
|
|
262
309
|
if (!result.ok)
|
|
263
310
|
process.exitCode = 1;
|
|
264
311
|
});
|
|
312
|
+
task
|
|
313
|
+
.command("reconcile")
|
|
314
|
+
.argument("<task-id>", "Task Pool task to reconcile after DAG abandon")
|
|
315
|
+
.requiredOption("--repo <repo-root>", "Target repo root")
|
|
316
|
+
.requiredOption("--feature-id <feature-id>", "Canonical Feature id")
|
|
317
|
+
.requiredOption("--worker-run-id <id>", "Worker run id bound to the DAG run")
|
|
318
|
+
.requiredOption("--dag-run-id <id>", "DAG run id to abandon")
|
|
319
|
+
.requiredOption("--action <action>", "Recovery action (only abandon is supported)")
|
|
320
|
+
.requiredOption("--reason <reason>", "Why the run is being abandoned")
|
|
321
|
+
.option("--force-abandon-recoverable", "Override refuse when DAG liveness/canResume indicates recoverable work (document operator intent in --reason)")
|
|
322
|
+
.description("Worker-aware abandon: DAG reconcile-run + Task Pool Failed projection")
|
|
323
|
+
.action(async (taskId, options) => {
|
|
324
|
+
if (options.action !== "abandon") {
|
|
325
|
+
throw new Error("task reconcile --action must be abandon");
|
|
326
|
+
}
|
|
327
|
+
try {
|
|
328
|
+
const result = await reconcileTaskPoolAbandon({
|
|
329
|
+
repoRoot: path.resolve(options.repo),
|
|
330
|
+
taskId,
|
|
331
|
+
featureId: options.featureId,
|
|
332
|
+
workerRunId: options.workerRunId,
|
|
333
|
+
dagRunId: options.dagRunId,
|
|
334
|
+
reason: options.reason,
|
|
335
|
+
forceAbandonRecoverable: options.forceAbandonRecoverable === true,
|
|
336
|
+
});
|
|
337
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
338
|
+
}
|
|
339
|
+
catch (error) {
|
|
340
|
+
handlePoolRecoveryCliError(error);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
265
343
|
task
|
|
266
344
|
.command("retry")
|
|
267
345
|
.argument("<task-id>", "Failed Task Pool task to requeue")
|
|
@@ -498,6 +576,11 @@ export async function main(argv = process.argv) {
|
|
|
498
576
|
async function readTaskYaml(taskSpecPath) {
|
|
499
577
|
return YAML.parse(await readFile(taskSpecPath, "utf-8"));
|
|
500
578
|
}
|
|
579
|
+
function handlePoolRecoveryCliError(error) {
|
|
580
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
581
|
+
process.stderr.write(`${detail}\n`);
|
|
582
|
+
process.exitCode = 1;
|
|
583
|
+
}
|
|
501
584
|
function handleFollowUpCliError(error, json, action) {
|
|
502
585
|
const detail = error instanceof Error ? error.message : String(error);
|
|
503
586
|
if (!json)
|
|
@@ -173,6 +173,71 @@ export async function finalizeGitTask(transaction, outcome) {
|
|
|
173
173
|
await assertClean(transaction.repoRoot);
|
|
174
174
|
return { status: "restored", taskId: outcome.taskSpec.id, workerRunId: outcome.workerRunId, artifactDir, changedFiles: changes };
|
|
175
175
|
}
|
|
176
|
+
/**
|
|
177
|
+
* Operator-only: advance recorded lastCheckpoint to current HEAD after manual
|
|
178
|
+
* revisions on the owned Feature branch. Does not append checkpoints[] entries.
|
|
179
|
+
*/
|
|
180
|
+
export async function advanceGitCheckpoint(input) {
|
|
181
|
+
const reason = input.reason.trim();
|
|
182
|
+
if (!reason)
|
|
183
|
+
throw new Error("feature git advance-checkpoint requires --reason <text>");
|
|
184
|
+
const repoRoot = await realpath(path.resolve(input.repoRoot));
|
|
185
|
+
assertSafeRuntimeId(input.featureId, "featureId");
|
|
186
|
+
const recordPath = transactionRecordPath(repoRoot, input.featureId);
|
|
187
|
+
const record = await readOptionalRecord(recordPath);
|
|
188
|
+
if (!record)
|
|
189
|
+
throw new Error(`Git transaction record not found for feature ${input.featureId}`);
|
|
190
|
+
if (record.featureId !== input.featureId) {
|
|
191
|
+
throw new Error(`Git transaction ownership mismatch: expected ${input.featureId}, found ${record.featureId}`);
|
|
192
|
+
}
|
|
193
|
+
await assertClean(repoRoot);
|
|
194
|
+
const branch = await git(repoRoot, ["branch", "--show-current"]);
|
|
195
|
+
if (branch !== record.branch) {
|
|
196
|
+
throw new Error(`current branch is outside the Feature transaction: expected ${record.branch}, got ${branch || "detached HEAD"}`);
|
|
197
|
+
}
|
|
198
|
+
const head = await git(repoRoot, ["rev-parse", "HEAD"]);
|
|
199
|
+
if (head === record.lastCheckpoint) {
|
|
200
|
+
const evidencePath = await writeAdvanceCheckpointEvidence(repoRoot, input.featureId, {
|
|
201
|
+
status: "unchanged",
|
|
202
|
+
reason,
|
|
203
|
+
previousCheckpoint: record.lastCheckpoint,
|
|
204
|
+
lastCheckpoint: head,
|
|
205
|
+
at: (input.now ?? new Date()).toISOString(),
|
|
206
|
+
});
|
|
207
|
+
return {
|
|
208
|
+
schemaVersion: 1,
|
|
209
|
+
featureId: input.featureId,
|
|
210
|
+
status: "unchanged",
|
|
211
|
+
previousCheckpoint: record.lastCheckpoint,
|
|
212
|
+
lastCheckpoint: head,
|
|
213
|
+
reason,
|
|
214
|
+
evidencePath: path.relative(repoRoot, evidencePath).split(path.sep).join("/"),
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
if (!await gitOk(repoRoot, ["merge-base", "--is-ancestor", record.lastCheckpoint, head])) {
|
|
218
|
+
throw new Error(`current HEAD is not descended from recorded checkpoint ${record.lastCheckpoint}`);
|
|
219
|
+
}
|
|
220
|
+
const previousCheckpoint = record.lastCheckpoint;
|
|
221
|
+
const updated = { ...record, lastCheckpoint: head };
|
|
222
|
+
await writeJsonAtomic(recordPath, updated);
|
|
223
|
+
const at = (input.now ?? new Date()).toISOString();
|
|
224
|
+
const evidencePath = await writeAdvanceCheckpointEvidence(repoRoot, input.featureId, {
|
|
225
|
+
status: "advanced",
|
|
226
|
+
reason,
|
|
227
|
+
previousCheckpoint,
|
|
228
|
+
lastCheckpoint: head,
|
|
229
|
+
at,
|
|
230
|
+
});
|
|
231
|
+
return {
|
|
232
|
+
schemaVersion: 1,
|
|
233
|
+
featureId: input.featureId,
|
|
234
|
+
status: "advanced",
|
|
235
|
+
previousCheckpoint,
|
|
236
|
+
lastCheckpoint: head,
|
|
237
|
+
reason,
|
|
238
|
+
evidencePath: path.relative(repoRoot, evidencePath).split(path.sep).join("/"),
|
|
239
|
+
};
|
|
240
|
+
}
|
|
176
241
|
export function transactionRecordPath(repoRoot, featureId) {
|
|
177
242
|
return path.join(getTaskPoolRoot(repoRoot), "artifacts", "features", featureId, "git", "transaction.json");
|
|
178
243
|
}
|
|
@@ -311,6 +376,16 @@ async function readIgnoredBaseline(repoRoot) {
|
|
|
311
376
|
function isSensitivePath(relative) {
|
|
312
377
|
return /(^|\/)(\.env(?:\.|$)|[^/]*\.(?:pem|key|p12|pfx))$/i.test(relative);
|
|
313
378
|
}
|
|
379
|
+
async function writeAdvanceCheckpointEvidence(repoRoot, featureId, payload) {
|
|
380
|
+
const evidenceDir = path.join(getTaskPoolRoot(repoRoot), "artifacts", "features", featureId, "git");
|
|
381
|
+
await mkdir(evidenceDir, { recursive: true });
|
|
382
|
+
const stamp = typeof payload.at === "string"
|
|
383
|
+
? payload.at.replace(/[:.]/g, "-")
|
|
384
|
+
: new Date().toISOString().replace(/[:.]/g, "-");
|
|
385
|
+
const evidencePath = path.join(evidenceDir, `advance-checkpoint-${stamp}.json`);
|
|
386
|
+
await writeJsonAtomic(evidencePath, { schemaVersion: 1, featureId, ...payload });
|
|
387
|
+
return evidencePath;
|
|
388
|
+
}
|
|
314
389
|
async function readOptionalRecord(filePath) {
|
|
315
390
|
try {
|
|
316
391
|
return gitTransactionRecordSchema.parse(JSON.parse(await readFile(filePath, "utf-8")));
|
|
@@ -3,6 +3,7 @@ import { mkdir, readFile, realpath, rename, unlink, writeFile } from "node:fs/pr
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { controllerIdentitiesMatch } from "../loop-agent/loop-agent-client.js";
|
|
6
|
+
import { artifactMatchesEvidenceToken, isShellVerificationToken, } from "../outcomes/evidence-tokens.js";
|
|
6
7
|
import { readVerifiedOutcome } from "../outcomes/store.js";
|
|
7
8
|
import { getTaskPoolRoot, readFeatureTaskPoolStates } from "../pool/run-store.js";
|
|
8
9
|
import { workflowSchema } from "../task-spec/workflow-routing.js";
|
|
@@ -30,7 +31,12 @@ const bundleOutcomeSchema = z
|
|
|
30
31
|
outcomePath: z.string().min(1),
|
|
31
32
|
outcomeSha256: z.string().regex(/^[a-f0-9]{64}$/),
|
|
32
33
|
acceptanceCoverage: z.array(z.string().min(1)),
|
|
33
|
-
artifacts: z.array(z.object({
|
|
34
|
+
artifacts: z.array(z.object({
|
|
35
|
+
path: z.string().min(1),
|
|
36
|
+
sha256: z.string().regex(/^[a-f0-9]{64}$/),
|
|
37
|
+
kind: z.string().min(1).optional(),
|
|
38
|
+
schemaId: z.string().min(1).optional(),
|
|
39
|
+
}).strict()),
|
|
34
40
|
integrationStatus: z.object({ mock: z.boolean(), real: z.boolean() }).strict(),
|
|
35
41
|
outcomeStatus: z.enum(["succeeded", "failed"]),
|
|
36
42
|
controllerIdentity: z
|
|
@@ -243,7 +249,12 @@ function projectBundleAcceptance(item, backendTests, frontendTests, implementati
|
|
|
243
249
|
return { acId: item.id, status: "covered", priority: item.priority };
|
|
244
250
|
}
|
|
245
251
|
function evidenceKindPresent(entry, kind) {
|
|
246
|
-
|
|
252
|
+
if (isShellVerificationToken(kind)) {
|
|
253
|
+
// Bundle outcomes do not embed shellVerification; shell evidence is
|
|
254
|
+
// already implied by outcomeStatus === "succeeded" for typed tests.
|
|
255
|
+
return entry.outcomeStatus === "succeeded";
|
|
256
|
+
}
|
|
257
|
+
return entry.artifacts.some((artifact) => artifactMatchesEvidenceToken(artifact, kind));
|
|
247
258
|
}
|
|
248
259
|
function isTypedVerificationWorkflow(workflow) {
|
|
249
260
|
return TYPED_VERIFICATION_WORKFLOWS.includes(workflow);
|
|
@@ -4,6 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import YAML from "yaml";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { getRunsJsonlPath, getTaskPoolRoot, readFeatureTaskPoolStates, readJsonlFile } from "../pool/run-store.js";
|
|
7
|
+
import { artifactMatchesEvidenceToken, isShellVerificationToken, } from "../outcomes/evidence-tokens.js";
|
|
7
8
|
import { readVerifiedOutcome } from "../outcomes/store.js";
|
|
8
9
|
import { acceptanceSpecSchema } from "../task-graph/acceptance-schema.js";
|
|
9
10
|
import { taskGraphSpecSchema } from "../task-graph/task-graph-schema.js";
|
|
@@ -591,9 +592,9 @@ export function projectDualCoverage(item, states, outcomes, failed, done, expect
|
|
|
591
592
|
return "awaiting-verification";
|
|
592
593
|
}
|
|
593
594
|
const requiredEvidence = item.verification.required_evidence ?? [];
|
|
594
|
-
if (requiredEvidence.some((
|
|
595
|
+
if (requiredEvidence.some((token) => !verEnvelopes.some(({ envelope }) => isShellVerificationToken(token)
|
|
595
596
|
? envelope.shellVerification?.exitZero === true
|
|
596
|
-
: envelope.artifacts?.some((artifact) => artifact
|
|
597
|
+
: envelope.artifacts?.some((artifact) => artifactMatchesEvidenceToken(artifact, token))))) {
|
|
597
598
|
return "awaiting-verification";
|
|
598
599
|
}
|
|
599
600
|
// 3. integration policy: real-required demands real evidence on every
|