@fitlab-ai/agent-infra 0.9.5 → 0.9.7
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/dist/bin/internal-cli.js +8 -3
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +171 -20
- package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +162 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +244 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -2
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +370 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +68 -8
- package/dist/lib/agent-clients/adapters/codex.js +1 -1
- package/dist/lib/defaults.json +10 -2
- package/dist/lib/git/workflow.js +30 -4
- package/dist/lib/internal/codex-lifecycle.js +169 -3
- package/dist/lib/internal/codex-sandbox-controller.js +106 -0
- package/dist/lib/internal/release-workflow.js +108 -11
- package/dist/lib/internal/sandbox-control.js +43 -6
- package/dist/lib/internal/task-create.js +12 -4
- package/dist/lib/internal/task-orchestration.js +40 -7
- package/dist/lib/platform/context.js +6 -4
- package/dist/lib/platform/github-client.js +55 -2
- package/dist/lib/platform/issue-comments.js +2 -1
- package/dist/lib/process-data/checkpoint.js +319 -0
- package/dist/lib/process-data/index.js +128 -60
- package/dist/lib/process-data/materialize.js +84 -0
- package/dist/lib/process-data/normalize.js +34 -1
- package/dist/lib/process-data/reconcile.js +55 -1
- package/dist/lib/process-data/sources.js +422 -1
- package/dist/lib/process-data/store.js +341 -11
- package/dist/lib/release/workflow.js +29 -21
- package/dist/lib/sandbox/commands/create.js +111 -60
- package/dist/lib/sandbox/commands/prune.js +40 -16
- package/dist/lib/sandbox/commands/rm.js +372 -94
- package/dist/lib/sandbox/commands/start.js +4 -3
- package/dist/lib/sandbox/control/client.js +138 -22
- package/dist/lib/sandbox/control/container-identity.js +64 -0
- package/dist/lib/sandbox/control/executor.js +142 -0
- package/dist/lib/sandbox/control/lifecycle.js +557 -0
- package/dist/lib/sandbox/control/protocol.js +69 -26
- package/dist/lib/sandbox/control/server.js +395 -99
- package/dist/lib/sandbox/control/state.js +191 -0
- package/dist/lib/sandbox/control/timing.js +8 -0
- package/dist/lib/sandbox/index.js +2 -2
- package/dist/lib/sandbox/managed-fs.js +14 -2
- package/dist/lib/sandbox/mounts.js +22 -19
- package/dist/lib/sandbox/recovery.js +284 -50
- package/dist/lib/sandbox/workspace-view.js +90 -10
- package/dist/lib/sandbox/worktree-safety.js +196 -0
- package/dist/lib/server/daemon.js +1 -1
- package/dist/lib/server/process-control.js +17 -1
- package/dist/lib/server/process-state.js +57 -20
- package/dist/lib/task/artifact-lifecycle.js +87 -15
- package/dist/lib/task/artifact-receipts.js +128 -0
- package/dist/lib/task/codex-orchestration.js +236 -0
- package/dist/lib/task/commands/status.js +3 -0
- package/dist/lib/task/commands/validate.js +289 -0
- package/dist/lib/task/commit-finalization.js +36 -9
- package/dist/lib/task/create.js +10 -2
- package/dist/lib/task/delegation-receipts.js +109 -2
- package/dist/lib/task/events.js +149 -6
- package/dist/lib/task/finalization-artifacts.js +2 -1
- package/dist/lib/task/implementation-inputs.js +1 -7
- package/dist/lib/task/index.js +9 -0
- package/dist/lib/task/ledger.js +13 -1
- package/dist/lib/task/orchestration.js +365 -15
- package/dist/lib/task/sections.js +5 -1
- package/dist/lib/task/task-execution-lock.js +5 -4
- package/dist/lib/task/verification-engine.js +78 -14
- package/dist/lib/task/verification.js +1 -0
- package/dist/lifecycle-build-manifest.json +6 -0
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +202 -18
- package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +227 -0
- package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +321 -0
- package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -3
- package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +26 -0
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +453 -0
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +83 -6
- package/lib/agent-clients/adapters/codex.ts +1 -1
- package/lib/defaults.json +10 -2
- package/lib/git/workflow.ts +25 -3
- package/lib/internal/codex-lifecycle.ts +175 -4
- package/lib/internal/codex-sandbox-controller.ts +109 -0
- package/lib/internal/release-workflow.ts +107 -11
- package/lib/internal/sandbox-control.ts +38 -6
- package/lib/internal/task-create.ts +16 -4
- package/lib/internal/task-orchestration.ts +34 -7
- package/lib/platform/context.ts +8 -5
- package/lib/platform/github-client.ts +61 -3
- package/lib/platform/issue-comments.ts +2 -1
- package/lib/process-data/checkpoint.ts +335 -0
- package/lib/process-data/index.ts +112 -60
- package/lib/process-data/materialize.ts +83 -0
- package/lib/process-data/normalize.ts +29 -1
- package/lib/process-data/reconcile.ts +64 -2
- package/lib/process-data/sources.ts +449 -2
- package/lib/process-data/store.ts +304 -11
- package/lib/process-data/types.ts +124 -2
- package/lib/release/workflow.ts +69 -20
- package/lib/sandbox/commands/create.ts +70 -25
- package/lib/sandbox/commands/prune.ts +53 -15
- package/lib/sandbox/commands/rm.ts +423 -108
- package/lib/sandbox/commands/start.ts +4 -3
- package/lib/sandbox/control/client.ts +152 -28
- package/lib/sandbox/control/container-identity.ts +93 -0
- package/lib/sandbox/control/executor.ts +167 -0
- package/lib/sandbox/control/lifecycle.ts +573 -0
- package/lib/sandbox/control/protocol.ts +121 -68
- package/lib/sandbox/control/server.ts +398 -109
- package/lib/sandbox/control/state.ts +213 -0
- package/lib/sandbox/control/timing.ts +12 -0
- package/lib/sandbox/index.ts +2 -2
- package/lib/sandbox/managed-fs.ts +17 -2
- package/lib/sandbox/mounts.ts +16 -8
- package/lib/sandbox/recovery.ts +335 -65
- package/lib/sandbox/workspace-view.ts +102 -11
- package/lib/sandbox/worktree-safety.ts +234 -0
- package/lib/server/daemon.ts +1 -1
- package/lib/server/process-control.ts +24 -3
- package/lib/server/process-state.ts +82 -27
- package/lib/task/artifact-lifecycle.ts +78 -14
- package/lib/task/artifact-receipts.ts +161 -0
- package/lib/task/codex-orchestration.ts +319 -0
- package/lib/task/commands/status.ts +2 -0
- package/lib/task/commands/validate.ts +282 -0
- package/lib/task/commit-finalization.ts +52 -9
- package/lib/task/create.ts +11 -2
- package/lib/task/delegation-receipts.ts +208 -7
- package/lib/task/events.ts +131 -5
- package/lib/task/finalization-artifacts.ts +2 -1
- package/lib/task/implementation-inputs.ts +1 -5
- package/lib/task/index.ts +8 -0
- package/lib/task/ledger.ts +16 -1
- package/lib/task/orchestration.ts +552 -18
- package/lib/task/sections.ts +6 -2
- package/lib/task/task-execution-lock.ts +11 -9
- package/lib/task/verification-engine.ts +93 -24
- package/lib/task/verification.ts +2 -1
- package/package.json +1 -1
- package/templates/.agents/README.en.md +7 -0
- package/templates/.agents/README.zh-CN.md +5 -0
- package/templates/.agents/hooks/lifecycle-delegation.js +72 -12
- package/templates/.agents/rules/lifecycle-orchestration.en.md +8 -2
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +8 -2
- package/templates/.agents/rules/no-mid-flow-questions.en.md +1 -0
- package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +1 -0
- package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
- package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +1 -1
- package/templates/.agents/rules/review-handshake.en.md +3 -1
- package/templates/.agents/rules/review-handshake.zh-CN.md +3 -1
- package/templates/.agents/skills/code-task/reference/dual-mode.en.md +3 -1
- package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +3 -1
- package/templates/.agents/skills/complete-task/SKILL.en.md +4 -0
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +4 -0
- package/templates/.agents/skills/post-release/SKILL.en.md +32 -6
- package/templates/.agents/skills/post-release/SKILL.zh-CN.md +32 -6
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +42 -0
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +42 -0
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +25 -0
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +25 -0
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +67 -0
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +67 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +69 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +69 -0
- package/templates/.agents/skills/run-task/SKILL.en.md +9 -7
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +9 -7
- package/templates/.agents/skills/run-task/reference/host-validation.en.md +5 -3
- package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +5 -3
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +75 -3
- package/templates/.claude/commands/run-manual-validation.en.md +9 -0
- package/templates/.claude/commands/run-manual-validation.zh-CN.md +9 -0
- package/templates/.codex/hooks.json +8 -8
- package/templates/.opencode/commands/run-manual-validation.en.md +11 -0
- package/templates/.opencode/commands/run-manual-validation.zh-CN.md +11 -0
- package/templates/.agents/workspace/README.en.md +0 -26
- package/templates/.agents/workspace/README.zh-CN.md +0 -26
|
@@ -4,7 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
import { locateActivityLog, appendActivityEntry, pairEntries, parseCommitAttemptStarted } from './activity-log.js';
|
|
5
5
|
import { commitIntentPath, digest, readCommitIntent } from './commit-intent.js';
|
|
6
6
|
import { parseTypedTaskFrontmatter } from './frontmatter.js';
|
|
7
|
-
import { parseReviewSummary } from './review-artifacts.js';
|
|
7
|
+
import { artifactName, maxRound, parseReviewSummary } from './review-artifacts.js';
|
|
8
8
|
import { extractReviewBaseline, extractReviewedSnapshotTree, findAuthoritativeReviewCodeArtifact, parseReviewedGitTree } from './review-fingerprint.js';
|
|
9
9
|
const SHA_RE = /^[a-f0-9]{40,64}$/;
|
|
10
10
|
function git(repoRoot, args) {
|
|
@@ -26,6 +26,22 @@ function isAncestor(repoRoot, ancestor, descendant) {
|
|
|
26
26
|
stdio: 'ignore'
|
|
27
27
|
}).status === 0;
|
|
28
28
|
}
|
|
29
|
+
function hasMatchingApprovedReview(taskDir, beforeRound, baseline, tree) {
|
|
30
|
+
const entries = fs.readdirSync(taskDir);
|
|
31
|
+
for (let round = Math.min(beforeRound - 1, maxRound(entries, 'review-code')); round >= 1; round -= 1) {
|
|
32
|
+
const file = path.join(taskDir, artifactName('review-code', round));
|
|
33
|
+
if (!fs.existsSync(file))
|
|
34
|
+
continue;
|
|
35
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
36
|
+
const summary = parseReviewSummary(content);
|
|
37
|
+
if (summary.ok
|
|
38
|
+
&& summary.summary.verdict === 'Approved'
|
|
39
|
+
&& extractReviewBaseline(content) === baseline
|
|
40
|
+
&& extractReviewedSnapshotTree(content) === tree)
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
29
45
|
function outcome(disposition, overrides = {}) {
|
|
30
46
|
return {
|
|
31
47
|
disposition,
|
|
@@ -34,6 +50,7 @@ function outcome(disposition, overrides = {}) {
|
|
|
34
50
|
currentHead: '',
|
|
35
51
|
committedHead: null,
|
|
36
52
|
commitNote: null,
|
|
53
|
+
activityNote: null,
|
|
37
54
|
needsAnchor: false,
|
|
38
55
|
needsLog: false,
|
|
39
56
|
intent: null,
|
|
@@ -130,6 +147,12 @@ function inspectCommitFinalization(taskDir, repoRoot, taskId) {
|
|
|
130
147
|
message: 'repository HEAD or ancestry conflicts with the commit intent'
|
|
131
148
|
});
|
|
132
149
|
}
|
|
150
|
+
const commitNote = git(repoRoot, ['show', '-s', '--format=%h%x20%s', intent.committedHead]);
|
|
151
|
+
const matchingDone = commitRows.filter((row) => row.done !== '' && row.note === commitNote);
|
|
152
|
+
const pushOnlyRetry = intent.phase === 'pushed'
|
|
153
|
+
&& intent.baselineHead === intent.committedHead
|
|
154
|
+
&& matchingDone.length === 1
|
|
155
|
+
&& openRows.length === 1;
|
|
133
156
|
const review = findAuthoritativeReviewCodeArtifact(taskDir);
|
|
134
157
|
let needsAnchor = false;
|
|
135
158
|
if (review.ok && review.path) {
|
|
@@ -147,7 +170,9 @@ function inspectCommitFinalization(taskDir, repoRoot, taskId) {
|
|
|
147
170
|
return invalid('authoritative review-code evidence is not a valid approved snapshot', currentHead, 'COMMIT_FINALIZATION_EVIDENCE_INVALID', shared);
|
|
148
171
|
}
|
|
149
172
|
const committedTree = git(repoRoot, ['rev-parse', `${intent.committedHead}^{tree}`]);
|
|
150
|
-
|
|
173
|
+
const matchingCurrentReview = reviewBaseline === intent.baselineHead && reviewTree === committedTree;
|
|
174
|
+
if (!matchingCurrentReview
|
|
175
|
+
&& !hasMatchingApprovedReview(taskDir, review.round, intent.baselineHead, committedTree)) {
|
|
151
176
|
return outcome('conflict', {
|
|
152
177
|
...shared,
|
|
153
178
|
code: 'COMMIT_FINALIZATION_CONFLICT',
|
|
@@ -155,18 +180,16 @@ function inspectCommitFinalization(taskDir, repoRoot, taskId) {
|
|
|
155
180
|
});
|
|
156
181
|
}
|
|
157
182
|
const anchor = String(metadata.last_reviewed_commit ?? '').trim();
|
|
158
|
-
if (anchor && anchor !== intent.committedHead) {
|
|
183
|
+
if (anchor && anchor !== intent.committedHead && anchor !== intent.baselineHead) {
|
|
159
184
|
return outcome('conflict', {
|
|
160
185
|
...shared,
|
|
161
186
|
code: 'COMMIT_FINALIZATION_CONFLICT',
|
|
162
187
|
message: 'task review anchor conflicts with the committed intent'
|
|
163
188
|
});
|
|
164
189
|
}
|
|
165
|
-
needsAnchor = anchor
|
|
190
|
+
needsAnchor = anchor !== intent.committedHead;
|
|
166
191
|
}
|
|
167
|
-
const
|
|
168
|
-
const matchingDone = commitRows.filter((row) => row.done !== '' && row.note === commitNote);
|
|
169
|
-
const needsLog = matchingDone.length === 0 && openRows.length === 1;
|
|
192
|
+
const needsLog = (matchingDone.length === 0 && openRows.length === 1) || pushOnlyRetry;
|
|
170
193
|
if (!(needsLog || (matchingDone.length === 1 && openRows.length === 0))) {
|
|
171
194
|
return outcome('conflict', {
|
|
172
195
|
...shared,
|
|
@@ -179,12 +202,16 @@ function inspectCommitFinalization(taskDir, repoRoot, taskId) {
|
|
|
179
202
|
...shared,
|
|
180
203
|
message: 'commit finalization can be recovered',
|
|
181
204
|
commitNote,
|
|
205
|
+
activityNote: pushOnlyRetry ? `Pushed ${commitNote}` : commitNote,
|
|
182
206
|
needsAnchor,
|
|
183
207
|
needsLog
|
|
184
208
|
});
|
|
185
209
|
}
|
|
186
210
|
function planCommitTaskFinalization(taskDir, inspection, agent, timestamp) {
|
|
187
|
-
if (inspection.disposition !== 'recoverable'
|
|
211
|
+
if (inspection.disposition !== 'recoverable'
|
|
212
|
+
|| !inspection.committedHead
|
|
213
|
+
|| !inspection.commitNote
|
|
214
|
+
|| !inspection.activityNote) {
|
|
188
215
|
throw new Error('commit finalization task mutations require a recoverable inspection');
|
|
189
216
|
}
|
|
190
217
|
const taskContent = fs.readFileSync(path.join(taskDir, 'task.md'), 'utf8');
|
|
@@ -207,7 +234,7 @@ function planCommitTaskFinalization(taskDir, inspection, agent, timestamp) {
|
|
|
207
234
|
time: timestamp,
|
|
208
235
|
step: 'Commit',
|
|
209
236
|
agent,
|
|
210
|
-
note: inspection.
|
|
237
|
+
note: inspection.activityNote
|
|
211
238
|
})
|
|
212
239
|
});
|
|
213
240
|
}
|
package/dist/lib/task/create.js
CHANGED
|
@@ -181,6 +181,13 @@ function assertRealDirectory(directory, code) {
|
|
|
181
181
|
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
182
182
|
throw new Error(code);
|
|
183
183
|
}
|
|
184
|
+
function ensureRealDirectory(directory, code) {
|
|
185
|
+
if (fs.existsSync(directory)) {
|
|
186
|
+
assertRealDirectory(directory, code);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
fs.mkdirSync(directory, { mode: 0o700 });
|
|
190
|
+
}
|
|
184
191
|
function withCreateLock(repoRoot, workspaceRoot, operation) {
|
|
185
192
|
const lockRoot = path.join(workspaceRoot, '.task-create.lock');
|
|
186
193
|
fs.mkdirSync(lockRoot, { recursive: true, mode: 0o700 });
|
|
@@ -254,8 +261,9 @@ function createLocalTask(value, options) {
|
|
|
254
261
|
const repoRoot = fs.realpathSync.native(options.repoRoot);
|
|
255
262
|
const workspaceRoot = path.join(repoRoot, '.agents', 'workspace');
|
|
256
263
|
const activeRoot = path.join(workspaceRoot, 'active');
|
|
257
|
-
|
|
258
|
-
|
|
264
|
+
ensureRealDirectory(path.join(repoRoot, '.agents'), 'TASK_CREATE_WORKSPACE_INVALID');
|
|
265
|
+
ensureRealDirectory(workspaceRoot, 'TASK_CREATE_WORKSPACE_INVALID');
|
|
266
|
+
ensureRealDirectory(activeRoot, 'TASK_CREATE_WORKSPACE_INVALID');
|
|
259
267
|
const project = readProject(repoRoot);
|
|
260
268
|
const canonical = canonicalTaskCreateCandidate(candidate);
|
|
261
269
|
const candidateDigest = sha256(canonical);
|
|
@@ -11,6 +11,7 @@ function fail(code, message) {
|
|
|
11
11
|
return { ok: false, code, message };
|
|
12
12
|
}
|
|
13
13
|
function prepareDelegation(input, options = {}) {
|
|
14
|
+
const monotonic = (options.monotonicNow ?? (() => Number(process.hrtime.bigint() / 1000000n)))();
|
|
14
15
|
return Object.freeze({
|
|
15
16
|
...input,
|
|
16
17
|
id: (options.id ?? randomUUID)(),
|
|
@@ -23,20 +24,59 @@ function prepareDelegation(input, options = {}) {
|
|
|
23
24
|
spawnMode: null,
|
|
24
25
|
agent: null,
|
|
25
26
|
status: 'prepared',
|
|
27
|
+
hostEvidence: null,
|
|
26
28
|
afterFingerprint: null,
|
|
27
29
|
changedPaths: Object.freeze([]),
|
|
28
30
|
createdAt: (options.now ?? (() => new Date().toISOString()))(),
|
|
31
|
+
preparedMonotonicMs: monotonic,
|
|
32
|
+
spawnDispatchMonotonicMs: null,
|
|
33
|
+
activationDeadlineMonotonicMs: null,
|
|
34
|
+
spawnDispatchedAt: null,
|
|
35
|
+
activationDeadlineAt: null,
|
|
36
|
+
startEvidenceMonotonicMs: null,
|
|
37
|
+
activatedMonotonicMs: null,
|
|
29
38
|
activatedAt: null,
|
|
30
39
|
sealedAt: null,
|
|
31
40
|
consumedAt: null
|
|
32
41
|
});
|
|
33
42
|
}
|
|
43
|
+
function dispatchDelegation(receipt, options = {}) {
|
|
44
|
+
if (receipt.status !== 'prepared') {
|
|
45
|
+
return fail('DELEGATION_STATE_INVALID', `delegation ${receipt.id} is ${receipt.status}, expected prepared`);
|
|
46
|
+
}
|
|
47
|
+
if (receipt.spawnDispatchMonotonicMs != null || receipt.spawnDispatchedAt != null) {
|
|
48
|
+
return fail('DELEGATION_DISPATCH_REPLAY', `delegation ${receipt.id} was already dispatched`);
|
|
49
|
+
}
|
|
50
|
+
const monotonic = (options.monotonicNow ?? (() => Number(process.hrtime.bigint() / 1000000n)))();
|
|
51
|
+
const dispatchedAt = (options.now ?? (() => new Date().toISOString()))();
|
|
52
|
+
const window = options.activationWindowMs ?? 15_000;
|
|
53
|
+
return { ok: true, receipt: Object.freeze({
|
|
54
|
+
...receipt,
|
|
55
|
+
spawnDispatchMonotonicMs: monotonic,
|
|
56
|
+
activationDeadlineMonotonicMs: monotonic + window,
|
|
57
|
+
spawnDispatchedAt: dispatchedAt,
|
|
58
|
+
activationDeadlineAt: new Date(Date.parse(dispatchedAt) + window).toISOString()
|
|
59
|
+
}) };
|
|
60
|
+
}
|
|
34
61
|
function activateDelegation(receipt, event, options = {}) {
|
|
35
62
|
const managedRole = managedDelegationRole(event.nativeAgent);
|
|
36
63
|
if (!managedRole)
|
|
37
64
|
return fail('DELEGATION_IGNORED', `subagent '${event.nativeAgent}' is not lifecycle-managed`);
|
|
38
65
|
if (receipt.status !== 'prepared')
|
|
39
66
|
return fail('DELEGATION_STATE_INVALID', `delegation ${receipt.id} is ${receipt.status}, expected prepared`);
|
|
67
|
+
if (receipt.spawnDispatchMonotonicMs == null
|
|
68
|
+
|| receipt.spawnDispatchedAt == null
|
|
69
|
+
|| receipt.activationDeadlineAt == null
|
|
70
|
+
|| receipt.activationDeadlineMonotonicMs == null
|
|
71
|
+
|| !Number.isFinite(Date.parse(receipt.spawnDispatchedAt))
|
|
72
|
+
|| !Number.isFinite(Date.parse(receipt.activationDeadlineAt))) {
|
|
73
|
+
return fail('DELEGATION_NOT_DISPATCHED', `delegation ${receipt.id} has not reached the native spawn dispatch boundary`);
|
|
74
|
+
}
|
|
75
|
+
const monotonic = (options.monotonicNow ?? (() => Number(process.hrtime.bigint() / 1000000n)))();
|
|
76
|
+
const wallNow = Date.parse((options.now ?? (() => new Date().toISOString()))());
|
|
77
|
+
if (wallNow > Date.parse(receipt.activationDeadlineAt)) {
|
|
78
|
+
return fail('DELEGATION_ACTIVATION_TIMEOUT', `delegation ${receipt.id} activation evidence arrived after its deadline`);
|
|
79
|
+
}
|
|
40
80
|
if (managedRole !== receipt.role)
|
|
41
81
|
return fail('DELEGATION_ROLE_MISMATCH', `managed role ${managedRole} does not match ${receipt.role}`);
|
|
42
82
|
if (!event.parentId || (receipt.parentId !== null && event.parentId !== receipt.parentId) || !event.childId || event.childId === event.parentId) {
|
|
@@ -55,6 +95,9 @@ function activateDelegation(receipt, event, options = {}) {
|
|
|
55
95
|
if (!actualReasoningEffort || actualReasoningEffort.trim() !== actualReasoningEffort) {
|
|
56
96
|
return fail('DELEGATION_REASONING_EFFORT_MISSING', 'native start event must provide a non-empty actual reasoning effort');
|
|
57
97
|
}
|
|
98
|
+
if (receipt.client === 'codex'
|
|
99
|
+
&& event.hostEvidence?.kind !== 'codex-lifecycle-v2')
|
|
100
|
+
return fail('DELEGATION_HOST_EVIDENCE_REQUIRED', 'Codex activation requires trusted lifecycle-v2 host evidence');
|
|
58
101
|
if (actualReasoningEffort !== receipt.requestedReasoningEffort
|
|
59
102
|
&& (!event.reasoningEffortFallbackReason || event.reasoningEffortFallbackReason.trim() === '')) {
|
|
60
103
|
return fail('DELEGATION_REASONING_EFFORT_FALLBACK_UNRECORDED', 'actual reasoning effort differs from requested effort without a fallback reason');
|
|
@@ -65,6 +108,38 @@ function activateDelegation(receipt, event, options = {}) {
|
|
|
65
108
|
if (actualReasoningEffort === receipt.requestedReasoningEffort && event.reasoningEffortFallbackReason) {
|
|
66
109
|
return fail('DELEGATION_REASONING_EFFORT_FALLBACK_INVALID', 'reasoning-effort fallback reason is only valid when actual effort differs');
|
|
67
110
|
}
|
|
111
|
+
if (event.hostEvidence
|
|
112
|
+
&& (receipt.client !== 'codex'
|
|
113
|
+
|| !event.hostEvidence.hookDefinitionHash.trim()
|
|
114
|
+
|| !Number.isSafeInteger(event.hostEvidence.startRevision)
|
|
115
|
+
|| event.hostEvidence.startRevision < 1))
|
|
116
|
+
return fail('DELEGATION_HOST_EVIDENCE_INVALID', 'Codex start evidence reference is invalid');
|
|
117
|
+
if (event.hostEvidence?.kind === 'codex-lifecycle-v2') {
|
|
118
|
+
const expected = receipt.lifecycleProvenance;
|
|
119
|
+
const spawnObservedAt = Date.parse(event.hostEvidence.spawnObservedAt ?? '');
|
|
120
|
+
const spawnDispatchedAt = Date.parse(receipt.spawnDispatchedAt);
|
|
121
|
+
const activationDeadlineAt = Date.parse(receipt.activationDeadlineAt);
|
|
122
|
+
if (!expected
|
|
123
|
+
|| event.hostEvidence.protocolVersion !== expected.protocolVersion
|
|
124
|
+
|| event.hostEvidence.packageVersion !== expected.packageVersion
|
|
125
|
+
|| event.hostEvidence.internalExecutableBuildHash !== expected.internalExecutableBuildHash
|
|
126
|
+
|| event.hostEvidence.lifecycleContractHash !== expected.lifecycleContractHash
|
|
127
|
+
|| event.hostEvidence.hookDefinitionHash !== expected.hookDefinitionHash
|
|
128
|
+
|| event.hostEvidence.hookSource !== expected.hookSource
|
|
129
|
+
|| event.hostEvidence.hookSourcePathDigest !== expected.hookSourcePathDigest
|
|
130
|
+
|| event.hostEvidence.hookSourceHash !== expected.hookSourceHash
|
|
131
|
+
|| event.hostEvidence.capabilitySessionId !== expected.capabilitySessionId
|
|
132
|
+
|| event.hostEvidence.capabilityTurnId !== expected.capabilityTurnId
|
|
133
|
+
|| !event.hostEvidence.spawnToolUseId?.trim()
|
|
134
|
+
|| event.hostEvidence.spawnToolUseId === expected.capabilityToolUseId
|
|
135
|
+
|| !Number.isFinite(spawnObservedAt)
|
|
136
|
+
|| spawnObservedAt < spawnDispatchedAt
|
|
137
|
+
|| spawnObservedAt > activationDeadlineAt
|
|
138
|
+
|| event.parentId !== expected.capabilitySessionId
|
|
139
|
+
|| (event.hostEvidence.controllerInstanceDigest ?? null) !== expected.controllerInstanceDigest
|
|
140
|
+
|| (event.hostEvidence.controlGeneration ?? null) !== expected.controlGeneration)
|
|
141
|
+
return fail('DELEGATION_HOST_EVIDENCE_INVALID', 'Codex lifecycle provenance does not match the prepared receipt');
|
|
142
|
+
}
|
|
68
143
|
return { ok: true, receipt: Object.freeze({
|
|
69
144
|
...receipt,
|
|
70
145
|
status: 'activated',
|
|
@@ -75,9 +150,26 @@ function activateDelegation(receipt, event, options = {}) {
|
|
|
75
150
|
actualReasoningEffort,
|
|
76
151
|
modelFallbackReason: event.modelFallbackReason ?? null,
|
|
77
152
|
reasoningEffortFallbackReason: event.reasoningEffortFallbackReason ?? null,
|
|
78
|
-
|
|
153
|
+
hostEvidence: event.hostEvidence ? Object.freeze({
|
|
154
|
+
...event.hostEvidence,
|
|
155
|
+
stopRevision: null,
|
|
156
|
+
consumer: null,
|
|
157
|
+
consumedAt: null
|
|
158
|
+
}) : receipt.hostEvidence ?? null,
|
|
159
|
+
startEvidenceMonotonicMs: monotonic,
|
|
160
|
+
activatedMonotonicMs: (options.monotonicNow ?? (() => Number(process.hrtime.bigint() / 1000000n)))(),
|
|
161
|
+
activatedAt: new Date(wallNow).toISOString()
|
|
79
162
|
}) };
|
|
80
163
|
}
|
|
164
|
+
function abortPreparedDelegation(receipt) {
|
|
165
|
+
if (receipt.status !== 'prepared') {
|
|
166
|
+
return fail('DELEGATION_STATE_INVALID', `delegation ${receipt.id} is ${receipt.status}, expected prepared`);
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
ok: true,
|
|
170
|
+
receipt: Object.freeze({ ...receipt, status: 'aborted' })
|
|
171
|
+
};
|
|
172
|
+
}
|
|
81
173
|
function completeDelegationStage(receipt, event) {
|
|
82
174
|
if (receipt.status !== 'activated')
|
|
83
175
|
return fail('DELEGATION_STATE_INVALID', `delegation ${receipt.id} is ${receipt.status}, expected activated`);
|
|
@@ -92,6 +184,9 @@ function completeDelegationStage(receipt, event) {
|
|
|
92
184
|
function sealDelegation(receipt, event, options = {}) {
|
|
93
185
|
if (receipt.status !== 'stage-completed')
|
|
94
186
|
return fail('DELEGATION_STATE_INVALID', `delegation ${receipt.id} is ${receipt.status}, expected stage-completed`);
|
|
187
|
+
if (receipt.client === 'codex' && options.requireHostEvidence !== false && !event.hostEvidence) {
|
|
188
|
+
return fail('DELEGATION_HOST_EVIDENCE_REQUIRED', 'Codex sealing requires consumed lifecycle-v2 host evidence');
|
|
189
|
+
}
|
|
95
190
|
if (event.childId !== receipt.childId || event.exitCode !== 0)
|
|
96
191
|
return fail('DELEGATION_STOP_INVALID', 'native stop identity or exit status is invalid');
|
|
97
192
|
if (receipt.role === 'reviewer') {
|
|
@@ -105,11 +200,23 @@ function sealDelegation(receipt, event, options = {}) {
|
|
|
105
200
|
if (disallowed)
|
|
106
201
|
return fail('DELEGATION_REVIEWER_WRITE_FORBIDDEN', `reviewer changed forbidden path '${disallowed}'`);
|
|
107
202
|
}
|
|
203
|
+
if (event.hostEvidence) {
|
|
204
|
+
if (!receipt.hostEvidence
|
|
205
|
+
|| !Number.isSafeInteger(event.hostEvidence.stopRevision)
|
|
206
|
+
|| event.hostEvidence.stopRevision <= receipt.hostEvidence.startRevision
|
|
207
|
+
|| event.hostEvidence.consumer !== receipt.id
|
|
208
|
+
|| !event.hostEvidence.consumedAt.trim())
|
|
209
|
+
return fail('DELEGATION_HOST_EVIDENCE_INVALID', 'Codex stop evidence reference is invalid');
|
|
210
|
+
}
|
|
108
211
|
return { ok: true, receipt: Object.freeze({
|
|
109
212
|
...receipt,
|
|
110
213
|
status: 'sealed',
|
|
111
214
|
afterFingerprint: event.afterFingerprint,
|
|
112
215
|
changedPaths: Object.freeze([...event.changedPaths]),
|
|
216
|
+
hostEvidence: event.hostEvidence && receipt.hostEvidence ? Object.freeze({
|
|
217
|
+
...receipt.hostEvidence,
|
|
218
|
+
...event.hostEvidence
|
|
219
|
+
}) : receipt.hostEvidence ?? null,
|
|
113
220
|
sealedAt: (options.now ?? (() => new Date().toISOString()))()
|
|
114
221
|
}) };
|
|
115
222
|
}
|
|
@@ -124,5 +231,5 @@ function consumeDelegation(receipt, options = {}) {
|
|
|
124
231
|
consumedAt: (options.now ?? (() => new Date().toISOString()))()
|
|
125
232
|
}) };
|
|
126
233
|
}
|
|
127
|
-
export { activateDelegation, completeDelegationStage, consumeDelegation, managedDelegationRole, prepareDelegation, sealDelegation };
|
|
234
|
+
export { activateDelegation, abortPreparedDelegation, completeDelegationStage, consumeDelegation, dispatchDelegation, managedDelegationRole, prepareDelegation, sealDelegation };
|
|
128
235
|
//# sourceMappingURL=delegation-receipts.js.map
|
package/dist/lib/task/events.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import { appendActivityEntry, locateActivityLog, pairEntries } from './activity-log.js';
|
|
3
|
-
import { buildArtifactLinkSection, artifactName, parseArtifactName, resolveArtifactContext, validateCompletedArtifact } from './artifact-lifecycle.js';
|
|
3
|
+
import { buildArtifactLinkSection, artifactName, inspectArtifactDirectory, parseReviewedInputReference, parseCodePlanInputReference, parseArtifactName, resolveArtifactContext, validateCompletedArtifact } from './artifact-lifecycle.js';
|
|
4
|
+
import { ArtifactReceiptError, sha256File, upsertArtifactReceipt } from './artifact-receipts.js';
|
|
4
5
|
import { parseTypedTaskFrontmatter } from './frontmatter.js';
|
|
5
6
|
import { consumeImplementationInput, IMPLEMENTATION_INPUT_ALIASES, parseImplementationInputs, renderImplementationInputs } from './implementation-inputs.js';
|
|
6
7
|
import { parseLedger, summarizeLedgerStage, validateLedgerRows } from './ledger.js';
|
|
@@ -18,7 +19,8 @@ const eventCatalog = [
|
|
|
18
19
|
'review-plan.started', 'review-plan.completed',
|
|
19
20
|
'code.started', 'code.completed',
|
|
20
21
|
'review-code.started', 'review-code.completed',
|
|
21
|
-
'manual-validation.started', 'manual-validation.completed'
|
|
22
|
+
'manual-validation.started', 'manual-validation.completed',
|
|
23
|
+
'validation-run.started', 'validation-run.completed'
|
|
22
24
|
];
|
|
23
25
|
const BASE_FIELDS = new Set(['taskRef', 'event', 'agent', 'dryRun']);
|
|
24
26
|
const SCHEMAS = {
|
|
@@ -36,7 +38,9 @@ const SCHEMAS = {
|
|
|
36
38
|
'review-code.started': { optional: ['round'] },
|
|
37
39
|
'review-code.completed': { required: ['artifact', 'verdict', 'blockers', 'major', 'minor', 'manualValidation'], optional: ['round', 'orchestrated'] },
|
|
38
40
|
'manual-validation.started': { optional: ['round'] },
|
|
39
|
-
'manual-validation.completed': { required: ['artifact', 'summaryResult'], optional: ['round'] }
|
|
41
|
+
'manual-validation.completed': { required: ['artifact', 'summaryResult'], optional: ['round'] },
|
|
42
|
+
'validation-run.started': { optional: ['round'] },
|
|
43
|
+
'validation-run.completed': { required: ['artifact'], optional: ['round'] }
|
|
40
44
|
};
|
|
41
45
|
function validateTaskEventRequest(request) {
|
|
42
46
|
if (!eventCatalog.includes(request.event))
|
|
@@ -84,7 +88,8 @@ const FAMILY = {
|
|
|
84
88
|
'review-plan': { artifact: 'review-plan', started: ['technical-design', 'technical-design-review'], completed: ['technical-design', 'technical-design-review'], target: 'technical-design-review', label: 'Review Plan' },
|
|
85
89
|
code: { artifact: 'code', started: ['technical-design-review', 'code-review'], completed: ['technical-design-review', 'code-review'], target: 'code', label: 'Code Task' },
|
|
86
90
|
'review-code': { artifact: 'review-code', started: ['code', 'code-review', 'commit'], completed: ['code', 'code-review', 'commit'], target: 'code-review', label: 'Review Code' },
|
|
87
|
-
'manual-validation': { artifact: 'manual-validation', started: ['code-review', 'commit'], completed: ['code-review', 'commit'], target: null, label: 'Complete Manual Validation' }
|
|
91
|
+
'manual-validation': { artifact: 'manual-validation', started: ['code-review', 'commit'], completed: ['code-review', 'commit'], target: null, label: 'Complete Manual Validation' },
|
|
92
|
+
'validation-run': { artifact: 'validation-run', started: ['code-review', 'commit'], completed: ['code-review', 'commit'], target: null, label: 'Run Manual Validation' }
|
|
88
93
|
};
|
|
89
94
|
const REVIEW_LEDGER_STAGES = {
|
|
90
95
|
'review-analysis': 'analysis',
|
|
@@ -180,6 +185,8 @@ function identity(request) {
|
|
|
180
185
|
note = `Fixed ${request.blockers} blockers, ${request.major} major, ${request.minor} minor issues${request.manualValidation ? `, skipped ${request.manualValidation} manual-validation` : ''} → ${request.artifact}`;
|
|
181
186
|
else if (family === 'code')
|
|
182
187
|
note = `Code implemented, ${request.filesModified} files modified, ${request.testsPassed} tests passed → ${request.artifact}`;
|
|
188
|
+
else if (family === 'validation-run')
|
|
189
|
+
note = `Validation evidence recorded → ${request.artifact}`;
|
|
183
190
|
else {
|
|
184
191
|
const verdict = request.verdict === 'approved' ? 'Approved' : request.verdict === 'changes-requested' ? 'Changes Requested' : 'Rejected';
|
|
185
192
|
note = `Verdict: ${verdict}, blockers: ${request.blockers}, major: ${request.major}, minor: ${request.minor}, Manual-validation: ${request.manualValidation} → ${request.artifact}`;
|
|
@@ -231,6 +238,92 @@ function openStartedIdentity(rows, family) {
|
|
|
231
238
|
});
|
|
232
239
|
return matches.length === 1 ? matches[0] : matches.length > 1 ? { conflict: true } : null;
|
|
233
240
|
}
|
|
241
|
+
function reviewInputFamily(family) {
|
|
242
|
+
return family === 'review-analysis' ? 'analysis' : family === 'review-plan' ? 'plan' : 'code';
|
|
243
|
+
}
|
|
244
|
+
function buildCompletionReceipt(taskDir, family, artifact, completedAt, frontmatter) {
|
|
245
|
+
if (family === 'code') {
|
|
246
|
+
let content;
|
|
247
|
+
try {
|
|
248
|
+
content = fs.readFileSync(artifact.path, 'utf8');
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
return { ok: false, message: `cannot read ${artifact.name}: ${String(error)}` };
|
|
252
|
+
}
|
|
253
|
+
const input = parseCodePlanInputReference(content);
|
|
254
|
+
if (!input)
|
|
255
|
+
return { ok: false, message: `${artifact.name} does not reference a canonical plan artifact` };
|
|
256
|
+
const startedInput = typeof frontmatter.code_input_artifact === 'string' ? frontmatter.code_input_artifact : '';
|
|
257
|
+
const startedSha256 = typeof frontmatter.code_input_sha256 === 'string' ? frontmatter.code_input_sha256 : '';
|
|
258
|
+
if (!startedInput || !startedSha256)
|
|
259
|
+
return { ok: false, message: 'code.started plan input context is missing' };
|
|
260
|
+
if (input.name !== startedInput)
|
|
261
|
+
return { ok: false, message: `${artifact.name} plan input '${input.name}' does not match code.started input '${startedInput}'` };
|
|
262
|
+
const plan = inspectArtifactDirectory(taskDir, 'plan');
|
|
263
|
+
if (plan.status !== 'ready' || !plan.latest || plan.latest.name !== input.name) {
|
|
264
|
+
return { ok: false, message: `code input '${input.name}' is not the latest plan artifact` };
|
|
265
|
+
}
|
|
266
|
+
try {
|
|
267
|
+
const inputSha256 = sha256File(plan.latest.path);
|
|
268
|
+
if (inputSha256 !== startedSha256)
|
|
269
|
+
return { ok: false, message: `code input ${input.name} changed after code.started` };
|
|
270
|
+
return {
|
|
271
|
+
ok: true,
|
|
272
|
+
receipt: {
|
|
273
|
+
event: 'code.completed', output: artifact.name, input: input.name,
|
|
274
|
+
inputSha256, completedAt
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
return { ok: false, message: `cannot hash code input plan: ${error instanceof Error ? error.message : String(error)}` };
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (!family.startsWith('review-'))
|
|
283
|
+
return null;
|
|
284
|
+
const expectedFamily = reviewInputFamily(family);
|
|
285
|
+
let content;
|
|
286
|
+
try {
|
|
287
|
+
content = fs.readFileSync(artifact.path, 'utf8');
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
return { ok: false, message: `cannot read ${artifact.name}: ${String(error)}` };
|
|
291
|
+
}
|
|
292
|
+
const input = parseReviewedInputReference(content, expectedFamily);
|
|
293
|
+
if (!input)
|
|
294
|
+
return { ok: false, message: `${artifact.name} does not reference a canonical ${expectedFamily} artifact` };
|
|
295
|
+
const startedInput = typeof frontmatter.review_input_artifact === 'string' ? frontmatter.review_input_artifact : '';
|
|
296
|
+
const startedSha256 = typeof frontmatter.review_input_sha256 === 'string' ? frontmatter.review_input_sha256 : '';
|
|
297
|
+
if (!startedInput || !startedSha256)
|
|
298
|
+
return { ok: false, message: 'review started input context is missing' };
|
|
299
|
+
if (input.name !== startedInput)
|
|
300
|
+
return { ok: false, message: `${artifact.name} input '${input.name}' does not match review.started input '${startedInput}'` };
|
|
301
|
+
const current = inspectArtifactDirectory(taskDir, expectedFamily);
|
|
302
|
+
if (current.status !== 'ready' || !current.latest || current.latest.name !== input.name) {
|
|
303
|
+
return { ok: false, message: `review input '${input.name}' is not the latest ${expectedFamily} artifact` };
|
|
304
|
+
}
|
|
305
|
+
const event = family === 'review-analysis'
|
|
306
|
+
? 'review-analysis.completed'
|
|
307
|
+
: family === 'review-plan' ? 'review-plan.completed' : 'review-code.completed';
|
|
308
|
+
try {
|
|
309
|
+
const stat = fs.lstatSync(current.latest.path);
|
|
310
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
311
|
+
throw new Error('review input is not a regular file');
|
|
312
|
+
const inputSha256 = sha256File(current.latest.path);
|
|
313
|
+
if (inputSha256 !== startedSha256)
|
|
314
|
+
return { ok: false, message: `review input ${input.name} changed after review.started` };
|
|
315
|
+
return {
|
|
316
|
+
ok: true,
|
|
317
|
+
receipt: {
|
|
318
|
+
event, output: artifact.name, input: input.name,
|
|
319
|
+
inputSha256, completedAt
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
return { ok: false, message: `cannot hash review input ${input.name}: ${error instanceof Error ? error.message : String(error)}` };
|
|
325
|
+
}
|
|
326
|
+
}
|
|
234
327
|
function applyTaskEventUnlocked(request, options = {}) {
|
|
235
328
|
const invalid = validateTaskEventRequest(request);
|
|
236
329
|
if (invalid)
|
|
@@ -329,7 +422,7 @@ function applyTaskEventUnlocked(request, options = {}) {
|
|
|
329
422
|
if (findingCountError)
|
|
330
423
|
return failed(normalized, findingCountError, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
331
424
|
let orchestrationCompletion = null;
|
|
332
|
-
if (eventIdentity.phase === 'completed' && eventIdentity.family !== 'manual-validation') {
|
|
425
|
+
if (eventIdentity.phase === 'completed' && eventIdentity.family !== 'manual-validation' && eventIdentity.family !== 'validation-run') {
|
|
333
426
|
const orchestrationStage = eventIdentity.family === 'analyze' ? 'analysis' : eventIdentity.family;
|
|
334
427
|
const execution = validateLifecycleExecution(normalized.taskRef, {
|
|
335
428
|
mode: normalized.orchestrated ? 'orchestrated' : 'standalone',
|
|
@@ -357,19 +450,69 @@ function applyTaskEventUnlocked(request, options = {}) {
|
|
|
357
450
|
catch (error) {
|
|
358
451
|
return failed(normalized, { code: 'METADATA_CAPTURE_FAILED', message: String(error) }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath });
|
|
359
452
|
}
|
|
453
|
+
let completionReceipt = null;
|
|
454
|
+
if (eventIdentity.phase === 'completed' && completedArtifact) {
|
|
455
|
+
const receipt = buildCompletionReceipt(resolved.taskDir, eventIdentity.family, completedArtifact, metadata.timestamp, frontmatter);
|
|
456
|
+
if (receipt && !receipt.ok) {
|
|
457
|
+
return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: receipt.message }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
458
|
+
}
|
|
459
|
+
completionReceipt = receipt?.receipt ?? null;
|
|
460
|
+
}
|
|
360
461
|
const step = eventIdentity.phase === 'started' || eventIdentity.target === null ? currentStep : eventIdentity.target;
|
|
361
462
|
const logStep = eventIdentity.phase === 'started' ? `${eventIdentity.action} [started]` : eventIdentity.action;
|
|
362
463
|
const body = appendActivityEntry(section, { time: metadata.timestamp, step: logStep, agent: normalized.agent, note: eventIdentity.note });
|
|
363
464
|
const frontmatterSet = { current_step: step, assigned_to: normalized.agent };
|
|
465
|
+
let frontmatterRemove;
|
|
466
|
+
if (eventIdentity.phase === 'started' && eventIdentity.family === 'code') {
|
|
467
|
+
const planInput = artifactContext?.inputs.find((input) => input.family === 'plan');
|
|
468
|
+
if (!planInput)
|
|
469
|
+
return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: 'code.started plan input context is unavailable' }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: step, action: eventIdentity.action, phase: eventIdentity.phase, artifactContext });
|
|
470
|
+
try {
|
|
471
|
+
frontmatterSet.code_input_artifact = planInput.name;
|
|
472
|
+
frontmatterSet.code_input_sha256 = sha256File(planInput.path);
|
|
473
|
+
}
|
|
474
|
+
catch (error) {
|
|
475
|
+
return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: `cannot hash code.started plan input: ${error instanceof Error ? error.message : String(error)}` }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: step, action: eventIdentity.action, phase: eventIdentity.phase, artifactContext });
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
else if (eventIdentity.phase === 'completed' && eventIdentity.family === 'code') {
|
|
479
|
+
frontmatterRemove = ['code_input_artifact', 'code_input_sha256'];
|
|
480
|
+
}
|
|
481
|
+
else if (eventIdentity.phase === 'started' && eventIdentity.family.startsWith('review-')) {
|
|
482
|
+
const expectedFamily = reviewInputFamily(eventIdentity.family);
|
|
483
|
+
const input = artifactContext?.inputs.find((candidate) => candidate.family === expectedFamily);
|
|
484
|
+
if (!input)
|
|
485
|
+
return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: `review.started ${expectedFamily} input context is unavailable` }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: step, action: eventIdentity.action, phase: eventIdentity.phase, artifactContext });
|
|
486
|
+
try {
|
|
487
|
+
frontmatterSet.review_input_artifact = input.name;
|
|
488
|
+
frontmatterSet.review_input_sha256 = sha256File(input.path);
|
|
489
|
+
}
|
|
490
|
+
catch (error) {
|
|
491
|
+
return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message: `cannot hash review.started input: ${error instanceof Error ? error.message : String(error)}` }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: step, action: eventIdentity.action, phase: eventIdentity.phase, artifactContext });
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
else if (eventIdentity.phase === 'completed' && eventIdentity.family.startsWith('review-')) {
|
|
495
|
+
frontmatterRemove = ['review_input_artifact', 'review_input_sha256'];
|
|
496
|
+
}
|
|
364
497
|
if (eventIdentity.phase === 'started' && normalized.implementationInput)
|
|
365
498
|
frontmatterSet.last_reviewed_commit = '';
|
|
366
499
|
const mutations = [
|
|
367
|
-
{ kind: 'frontmatter', set: frontmatterSet }
|
|
500
|
+
{ kind: 'frontmatter', set: frontmatterSet, remove: frontmatterRemove }
|
|
368
501
|
];
|
|
369
502
|
if (completedArtifact) {
|
|
370
503
|
const link = buildArtifactLinkSection(content, completedArtifact);
|
|
371
504
|
mutations.push({ kind: 'section', aliases: link.aliases, heading: link.heading, body: link.body });
|
|
372
505
|
}
|
|
506
|
+
if (completionReceipt) {
|
|
507
|
+
try {
|
|
508
|
+
const receiptSection = upsertArtifactReceipt(content, completionReceipt);
|
|
509
|
+
mutations.push({ kind: 'section', aliases: receiptSection.aliases, heading: receiptSection.heading, body: receiptSection.body });
|
|
510
|
+
}
|
|
511
|
+
catch (error) {
|
|
512
|
+
const message = error instanceof ArtifactReceiptError ? error.message : String(error);
|
|
513
|
+
return failed(normalized, { code: 'EVENT_ARTIFACT_CONFLICT', message }, { taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, fromStep: currentStep, toStep: currentStep, action: eventIdentity.action, phase: eventIdentity.phase });
|
|
514
|
+
}
|
|
515
|
+
}
|
|
373
516
|
if (eventIdentity.phase === 'completed' && normalized.implementationInput) {
|
|
374
517
|
let implementationRows;
|
|
375
518
|
try {
|
|
@@ -114,14 +114,8 @@ function finalizeImplementationInput(rows, id, decisionEvidence, decidedAt) {
|
|
|
114
114
|
next.forEach(validateInput);
|
|
115
115
|
return next;
|
|
116
116
|
}
|
|
117
|
-
function selectPendingImplementationInput(rows
|
|
118
|
-
const reviewEpoch = Date.parse(reviewCompletedAt.replace(' ', 'T'));
|
|
119
|
-
if (!Number.isFinite(reviewEpoch))
|
|
120
|
-
throw new Error('latest review completion time is invalid');
|
|
117
|
+
function selectPendingImplementationInput(rows) {
|
|
121
118
|
const pending = rows.filter((row) => row.needsImplementation && row.status === 'pending');
|
|
122
|
-
const stale = pending.find((row) => Date.parse(row.decidedAt.replace(' ', 'T')) <= reviewEpoch);
|
|
123
|
-
if (stale)
|
|
124
|
-
throw new Error(`implementation input ${stale.id} is not later than the latest approved review`);
|
|
125
119
|
return [...pending].sort((left, right) => {
|
|
126
120
|
const time = Date.parse(left.decidedAt.replace(' ', 'T')) - Date.parse(right.decidedAt.replace(' ', 'T'));
|
|
127
121
|
return time || Number.parseInt(left.id.slice(3), 10) - Number.parseInt(right.id.slice(3), 10);
|
package/dist/lib/task/index.js
CHANGED
|
@@ -10,6 +10,7 @@ Commands:
|
|
|
10
10
|
ls [--all | --blocked | --completed] List tasks (default: active)
|
|
11
11
|
show [--task <ref>] Print a task.md
|
|
12
12
|
status [--task <ref>] Aggregated status view (metadata / artifacts / git / platform)
|
|
13
|
+
validate <ref> [options] -- <command> Run host validation in snapshot or inplace scope
|
|
13
14
|
|
|
14
15
|
Examples:
|
|
15
16
|
ai task cat 11 analysis
|
|
@@ -29,6 +30,7 @@ Examples:
|
|
|
29
30
|
ai task show 11
|
|
30
31
|
ai task show TASK-20260612-162737
|
|
31
32
|
ai task status 11
|
|
33
|
+
ai task validate 11 -- npm test
|
|
32
34
|
|
|
33
35
|
Run 'ai task <command> --help' for details.`;
|
|
34
36
|
export async function runTask(args) {
|
|
@@ -89,6 +91,13 @@ export async function runTask(args) {
|
|
|
89
91
|
status(rest);
|
|
90
92
|
break;
|
|
91
93
|
}
|
|
94
|
+
case 'validate': {
|
|
95
|
+
const { validate } = await import('./commands/validate.js');
|
|
96
|
+
const exitCode = await validate(rest);
|
|
97
|
+
if (exitCode !== 0)
|
|
98
|
+
process.exitCode = exitCode;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
92
101
|
default:
|
|
93
102
|
process.stderr.write(`Unknown task command: ${subcommand}\n\n`);
|
|
94
103
|
process.stdout.write(`${USAGE}\n`);
|
package/dist/lib/task/ledger.js
CHANGED
|
@@ -31,8 +31,20 @@ function validateLedgerRows(rows) {
|
|
|
31
31
|
if (seen.has(row.id))
|
|
32
32
|
return { code: 'LEDGER_DUPLICATE_ID', message: `duplicate ledger id '${row.id}'` };
|
|
33
33
|
seen.add(row.id);
|
|
34
|
-
if (/^PRC-[1-9]\d*$/.test(row.id))
|
|
34
|
+
if (/^PRC-[1-9]\d*$/.test(row.id)) {
|
|
35
|
+
if (row.stage !== 'post-review-commit' ||
|
|
36
|
+
row.round !== '-' ||
|
|
37
|
+
row.severity !== '-' ||
|
|
38
|
+
row.status !== 'human-decided' ||
|
|
39
|
+
row.evidence === '' ||
|
|
40
|
+
/[\r\n]/.test(row.evidence)) {
|
|
41
|
+
return { code: 'LEDGER_DOCUMENT_INVALID', message: `post-review exemption row '${row.id}' is invalid` };
|
|
42
|
+
}
|
|
35
43
|
continue;
|
|
44
|
+
}
|
|
45
|
+
if (row.stage === 'post-review-commit') {
|
|
46
|
+
return { code: 'LEDGER_DOCUMENT_INVALID', message: `post-review exemption row '${row.id}' must use a PRC id` };
|
|
47
|
+
}
|
|
36
48
|
if (!/^(AN|PL|CD|HD)-[1-9]\d*$/.test(row.id))
|
|
37
49
|
return { code: 'LEDGER_ID_INVALID', message: `ledger id '${row.id}' is invalid` };
|
|
38
50
|
if (!['analysis', 'plan', 'code', 'post-review-commit'].includes(row.stage))
|