@fitlab-ai/agent-infra 0.9.3 → 0.9.4
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/README.md +2 -0
- package/README.zh-CN.md +2 -0
- package/bin/cli.ts +12 -0
- package/dist/bin/cli.js +14 -0
- package/dist/bin/internal-cli.js +134 -123
- package/dist/lib/agent-clients/adapter.js +92 -2
- package/dist/lib/{sandbox/credentials.js → agent-clients/adapters/claude-code-credentials.js} +4 -14
- package/dist/lib/agent-clients/adapters/claude-code-refresh.js +85 -0
- package/dist/lib/agent-clients/adapters/claude-code-sandbox.js +247 -0
- package/dist/lib/agent-clients/adapters/claude-code.js +17 -3
- package/dist/lib/agent-clients/adapters/opencode-sandbox.js +108 -0
- package/dist/lib/agent-clients/adapters/opencode.js +15 -3
- package/dist/lib/agent-clients/adapters/runtimes/claude-code.dockerfile +65 -0
- package/dist/lib/agent-clients/registry.js +9 -1
- package/dist/lib/internal/sandbox-control.js +24 -0
- package/dist/lib/internal/task-activity.js +45 -13
- package/dist/lib/internal/task-ledger.js +12 -3
- package/dist/lib/internal/task-lifecycle.js +19 -1
- package/dist/lib/internal/task-orchestration.js +122 -18
- package/dist/lib/platform/github-client.js +19 -2
- package/dist/lib/platform/pr-checks.js +1 -1
- package/dist/lib/platform/pull-requests.js +47 -1
- package/dist/lib/platform/releases.js +13 -5
- package/dist/lib/process-data/index.js +239 -0
- package/dist/lib/process-data/normalize.js +198 -0
- package/dist/lib/process-data/privacy.js +33 -0
- package/dist/lib/process-data/reconcile.js +95 -0
- package/dist/lib/process-data/repair.js +58 -0
- package/dist/lib/process-data/sources.js +345 -0
- package/dist/lib/process-data/store.js +282 -0
- package/dist/lib/process-data/types.js +2 -0
- package/dist/lib/run/index.js +4 -3
- package/dist/lib/run/tui.js +7 -8
- package/dist/lib/sandbox/agent-client-reconciler.js +26 -2
- package/dist/lib/sandbox/capture.js +7 -2
- package/dist/lib/sandbox/commands/create.js +109 -228
- package/dist/lib/sandbox/commands/enter.js +15 -47
- package/dist/lib/sandbox/commands/list-running.js +16 -6
- package/dist/lib/sandbox/commands/ls.js +10 -6
- package/dist/lib/sandbox/commands/rebuild.js +4 -3
- package/dist/lib/sandbox/commands/refresh.js +4 -84
- package/dist/lib/sandbox/commands/rm.js +24 -3
- package/dist/lib/sandbox/commands/show.js +12 -3
- package/dist/lib/sandbox/commands/start.js +7 -4
- package/dist/lib/sandbox/config.js +2 -0
- package/dist/lib/sandbox/constants.js +6 -0
- package/dist/lib/sandbox/control/client.js +44 -0
- package/dist/lib/sandbox/control/protocol.js +35 -0
- package/dist/lib/sandbox/control/server.js +109 -0
- package/dist/lib/sandbox/dockerfile.js +36 -13
- package/dist/lib/sandbox/mounts.js +17 -4
- package/dist/lib/sandbox/recovery.js +128 -5
- package/dist/lib/sandbox/redaction.js +12 -0
- package/dist/lib/sandbox/runtimes/base.dockerfile +24 -74
- package/dist/lib/sandbox/workspace-identity.js +95 -0
- package/dist/lib/sandbox/workspace-view.js +103 -0
- package/dist/lib/task/activity-intent.js +257 -51
- package/dist/lib/task/commit-finalization.js +267 -0
- package/dist/lib/task/commit-intent.js +165 -0
- package/dist/lib/task/decision-intents.js +31 -17
- package/dist/lib/task/delegation-receipts.js +2 -2
- package/dist/lib/task/implementation-inputs.js +54 -7
- package/dist/lib/task/ledger-intents.js +35 -1
- package/dist/lib/task/orchestration.js +363 -28
- package/dist/lib/task/resolve-ref.js +41 -1
- package/lib/agent-clients/adapter.ts +126 -2
- package/lib/{sandbox/credentials.ts → agent-clients/adapters/claude-code-credentials.ts} +3 -18
- package/lib/agent-clients/adapters/claude-code-refresh.ts +128 -0
- package/lib/agent-clients/adapters/claude-code-sandbox.ts +295 -0
- package/lib/agent-clients/adapters/claude-code.ts +21 -3
- package/lib/agent-clients/adapters/opencode-sandbox.ts +136 -0
- package/lib/agent-clients/adapters/opencode.ts +18 -3
- package/lib/agent-clients/adapters/runtimes/claude-code.dockerfile +65 -0
- package/lib/agent-clients/registry.ts +9 -1
- package/lib/internal/sandbox-control.ts +24 -0
- package/lib/internal/task-activity.ts +50 -14
- package/lib/internal/task-ledger.ts +12 -3
- package/lib/internal/task-lifecycle.ts +22 -1
- package/lib/internal/task-orchestration.ts +81 -11
- package/lib/platform/github-client.ts +20 -2
- package/lib/platform/pr-checks.ts +1 -1
- package/lib/platform/pull-requests.ts +49 -1
- package/lib/platform/releases.ts +16 -4
- package/lib/process-data/index.ts +224 -0
- package/lib/process-data/normalize.ts +202 -0
- package/lib/process-data/privacy.ts +41 -0
- package/lib/process-data/reconcile.ts +110 -0
- package/lib/process-data/repair.ts +64 -0
- package/lib/process-data/sources.ts +354 -0
- package/lib/process-data/store.ts +315 -0
- package/lib/process-data/types.ts +135 -0
- package/lib/run/index.ts +5 -3
- package/lib/run/tui.ts +11 -8
- package/lib/sandbox/agent-client-reconciler.ts +44 -2
- package/lib/sandbox/capture.ts +9 -2
- package/lib/sandbox/commands/create.ts +126 -260
- package/lib/sandbox/commands/enter.ts +13 -59
- package/lib/sandbox/commands/list-running.ts +23 -6
- package/lib/sandbox/commands/ls.ts +22 -6
- package/lib/sandbox/commands/rebuild.ts +4 -3
- package/lib/sandbox/commands/refresh.ts +4 -126
- package/lib/sandbox/commands/rm.ts +24 -3
- package/lib/sandbox/commands/show.ts +29 -3
- package/lib/sandbox/commands/start.ts +6 -4
- package/lib/sandbox/config.ts +4 -0
- package/lib/sandbox/constants.ts +8 -0
- package/lib/sandbox/control/client.ts +55 -0
- package/lib/sandbox/control/protocol.ts +75 -0
- package/lib/sandbox/control/server.ts +117 -0
- package/lib/sandbox/dockerfile.ts +66 -13
- package/lib/sandbox/mounts.ts +24 -4
- package/lib/sandbox/recovery.ts +163 -6
- package/lib/sandbox/redaction.ts +14 -0
- package/lib/sandbox/runtimes/base.dockerfile +24 -74
- package/lib/sandbox/tool-types.ts +16 -0
- package/lib/sandbox/workspace-identity.ts +123 -0
- package/lib/sandbox/workspace-view.ts +143 -0
- package/lib/task/activity-intent.ts +350 -61
- package/lib/task/commit-finalization.ts +356 -0
- package/lib/task/commit-intent.ts +230 -0
- package/lib/task/decision-intents.ts +32 -12
- package/lib/task/delegation-receipts.ts +2 -2
- package/lib/task/implementation-inputs.ts +62 -8
- package/lib/task/ledger-intents.ts +43 -3
- package/lib/task/orchestration.ts +501 -33
- package/lib/task/resolve-ref.ts +46 -1
- package/package.json +1 -1
- package/templates/.agents/rules/README.en.md +1 -1
- package/templates/.agents/rules/README.zh-CN.md +1 -1
- package/templates/.agents/rules/human-decision-context.en.md +11 -2
- package/templates/.agents/rules/human-decision-context.zh-CN.md +11 -2
- package/templates/.agents/rules/pr-checks-commands.en.md +1 -1
- package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
- package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +2 -2
- package/templates/.agents/rules/pr-checks-commands.zh-CN.md +1 -1
- package/templates/.agents/rules/review-handshake.zh-CN.md +1 -1
- package/templates/.agents/rules/task-management.en.md +2 -0
- package/templates/.agents/rules/task-management.zh-CN.md +2 -0
- package/templates/.agents/skills/code-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/commit/SKILL.en.md +21 -11
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +27 -11
- package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +49 -0
- package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +55 -0
- package/templates/.agents/skills/commit/reference/task-status-update.en.md +4 -4
- package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +4 -4
- package/templates/.agents/skills/complete-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/create-pr/SKILL.en.md +4 -4
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/create-task/reference/context-capture.en.md +34 -0
- package/templates/.agents/skills/create-task/reference/context-capture.zh-CN.md +34 -0
- package/templates/.agents/skills/review-code/SKILL.en.md +2 -2
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/review-code/reference/output-templates.en.md +1 -1
- package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +1 -1
- package/templates/.agents/skills/review-pr/SKILL.en.md +36 -8
- package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +36 -8
- package/templates/.agents/skills/review-pr/reference/report-template.en.md +1 -1
- package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +1 -1
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +67 -50
- package/templates/.agents/skills/watch-pr/SKILL.en.md +4 -4
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +2 -2
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +2 -2
- package/templates/.agents/templates/task.en.md +1 -1
- package/templates/.agents/templates/task.zh-CN.md +1 -1
- package/templates/.claude/commands/watch-pr.en.md +1 -1
- package/templates/.claude/commands/watch-pr.zh-CN.md +1 -1
- package/templates/.claude/rules/tool-preferences.en.md +20 -0
- package/templates/.claude/rules/tool-preferences.zh-CN.md +20 -0
- package/templates/.opencode/commands/watch-pr.en.md +1 -1
- package/templates/.opencode/commands/watch-pr.zh-CN.md +1 -1
- package/templates/.gemini/commands/_project_/review-pr.en.toml +0 -10
- package/templates/.gemini/commands/_project_/review-pr.zh-CN.toml +0 -10
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { locateActivityLog, appendActivityEntry, pairEntries } from './activity-log.js';
|
|
5
|
+
import { commitIntentPath, digest, readCommitIntent } from './commit-intent.js';
|
|
6
|
+
import { parseTypedTaskFrontmatter } from './frontmatter.js';
|
|
7
|
+
import { parseReviewSummary } from './review-artifacts.js';
|
|
8
|
+
import { extractReviewBaseline, extractReviewedSnapshotTree, findAuthoritativeReviewCodeArtifact } from './review-fingerprint.js';
|
|
9
|
+
const SHA_RE = /^[a-f0-9]{40,64}$/;
|
|
10
|
+
function git(repoRoot, args) {
|
|
11
|
+
return execFileSync('git', args, {
|
|
12
|
+
cwd: repoRoot,
|
|
13
|
+
encoding: 'utf8',
|
|
14
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
15
|
+
}).trim();
|
|
16
|
+
}
|
|
17
|
+
function commitExists(repoRoot, value) {
|
|
18
|
+
return SHA_RE.test(value) && spawnSync('git', ['cat-file', '-e', `${value}^{commit}`], {
|
|
19
|
+
cwd: repoRoot,
|
|
20
|
+
stdio: 'ignore'
|
|
21
|
+
}).status === 0;
|
|
22
|
+
}
|
|
23
|
+
function isAncestor(repoRoot, ancestor, descendant) {
|
|
24
|
+
return spawnSync('git', ['merge-base', '--is-ancestor', ancestor, descendant], {
|
|
25
|
+
cwd: repoRoot,
|
|
26
|
+
stdio: 'ignore'
|
|
27
|
+
}).status === 0;
|
|
28
|
+
}
|
|
29
|
+
function outcome(disposition, overrides = {}) {
|
|
30
|
+
return {
|
|
31
|
+
disposition,
|
|
32
|
+
code: null,
|
|
33
|
+
message: disposition,
|
|
34
|
+
currentHead: '',
|
|
35
|
+
committedHead: null,
|
|
36
|
+
commitNote: null,
|
|
37
|
+
needsAnchor: false,
|
|
38
|
+
needsLog: false,
|
|
39
|
+
intent: null,
|
|
40
|
+
intentDigest: null,
|
|
41
|
+
...overrides
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function invalid(message, currentHead = '', code = 'COMMIT_FINALIZATION_EVIDENCE_INVALID', overrides = {}) {
|
|
45
|
+
return outcome('invalid', { code, message, currentHead, ...overrides });
|
|
46
|
+
}
|
|
47
|
+
function inspectCommitFinalization(taskDir, repoRoot, taskId) {
|
|
48
|
+
let currentHead;
|
|
49
|
+
let taskContent;
|
|
50
|
+
let metadata;
|
|
51
|
+
try {
|
|
52
|
+
currentHead = git(repoRoot, ['rev-parse', 'HEAD']);
|
|
53
|
+
taskContent = fs.readFileSync(path.join(taskDir, 'task.md'), 'utf8');
|
|
54
|
+
metadata = parseTypedTaskFrontmatter(taskContent);
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
return invalid(error instanceof Error ? error.message : String(error));
|
|
58
|
+
}
|
|
59
|
+
const activity = locateActivityLog(taskContent);
|
|
60
|
+
if (!activity)
|
|
61
|
+
return invalid('task activity log is missing or ambiguous', currentHead);
|
|
62
|
+
const commitRows = pairEntries(activity.entries).filter((row) => row.step === 'Commit');
|
|
63
|
+
const openRows = commitRows.filter((row) => row.started !== '' && row.done === '');
|
|
64
|
+
const file = commitIntentPath(taskDir);
|
|
65
|
+
if (!fs.existsSync(file)) {
|
|
66
|
+
if (openRows.length > 0) {
|
|
67
|
+
return outcome('orphaned-start', {
|
|
68
|
+
code: 'COMMIT_FINALIZATION_EVIDENCE_MISSING',
|
|
69
|
+
message: 'an open Commit activity entry exists without a commit intent',
|
|
70
|
+
currentHead
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return outcome('idle', { message: 'no active commit finalization exists', currentHead });
|
|
74
|
+
}
|
|
75
|
+
let raw;
|
|
76
|
+
let intent;
|
|
77
|
+
try {
|
|
78
|
+
raw = fs.readFileSync(file, 'utf8');
|
|
79
|
+
intent = readCommitIntent(taskDir, taskId);
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
return invalid(error instanceof Error ? error.message : String(error), currentHead);
|
|
83
|
+
}
|
|
84
|
+
const intentDigest = digest(raw);
|
|
85
|
+
const shared = { currentHead, intent, intentDigest, committedHead: intent.committedHead };
|
|
86
|
+
if (!commitExists(repoRoot, intent.baselineHead)) {
|
|
87
|
+
return invalid('commit intent baseline is not a valid Git commit', currentHead, 'COMMIT_FINALIZATION_EVIDENCE_INVALID', shared);
|
|
88
|
+
}
|
|
89
|
+
if (intent.phase === 'prepared') {
|
|
90
|
+
if (intent.committedHead !== null || intent.pushEvidence !== null) {
|
|
91
|
+
return invalid('prepared commit intent contains commit or push evidence', currentHead, 'COMMIT_FINALIZATION_EVIDENCE_INVALID', shared);
|
|
92
|
+
}
|
|
93
|
+
if (currentHead !== intent.baselineHead || openRows.length !== 1) {
|
|
94
|
+
return outcome('conflict', {
|
|
95
|
+
...shared,
|
|
96
|
+
code: 'COMMIT_FINALIZATION_CONFLICT',
|
|
97
|
+
message: 'prepared commit intent does not match HEAD and exactly one open Commit entry'
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return outcome('prepared', { ...shared, message: 'prepared commit intent can be safely retried' });
|
|
101
|
+
}
|
|
102
|
+
if (!intent.committedHead || !commitExists(repoRoot, intent.committedHead)) {
|
|
103
|
+
return invalid('committed intent head is missing or invalid', currentHead, 'COMMIT_FINALIZATION_EVIDENCE_INVALID', shared);
|
|
104
|
+
}
|
|
105
|
+
if (intent.phase === 'committed' && intent.pushEvidence !== null) {
|
|
106
|
+
return invalid('committed intent contains premature push evidence', currentHead, 'COMMIT_FINALIZATION_EVIDENCE_INVALID', shared);
|
|
107
|
+
}
|
|
108
|
+
if (intent.phase === 'pushed'
|
|
109
|
+
&& (intent.pushEvidence === null
|
|
110
|
+
|| intent.pushEvidence.head !== intent.committedHead
|
|
111
|
+
|| !intent.pushEvidence.remote
|
|
112
|
+
|| !intent.pushEvidence.ref)) {
|
|
113
|
+
return invalid('pushed intent evidence is missing or inconsistent', currentHead, 'COMMIT_FINALIZATION_EVIDENCE_INVALID', shared);
|
|
114
|
+
}
|
|
115
|
+
if (currentHead !== intent.committedHead || !isAncestor(repoRoot, intent.baselineHead, intent.committedHead)) {
|
|
116
|
+
return outcome('conflict', {
|
|
117
|
+
...shared,
|
|
118
|
+
code: 'COMMIT_FINALIZATION_CONFLICT',
|
|
119
|
+
message: 'repository HEAD or ancestry conflicts with the commit intent'
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
const review = findAuthoritativeReviewCodeArtifact(taskDir);
|
|
123
|
+
let needsAnchor = false;
|
|
124
|
+
if (review.ok && review.path) {
|
|
125
|
+
let reviewContent;
|
|
126
|
+
try {
|
|
127
|
+
reviewContent = fs.readFileSync(review.path, 'utf8');
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
return invalid(error instanceof Error ? error.message : String(error), currentHead, 'COMMIT_FINALIZATION_EVIDENCE_MISSING', shared);
|
|
131
|
+
}
|
|
132
|
+
const summary = parseReviewSummary(reviewContent);
|
|
133
|
+
const reviewBaseline = extractReviewBaseline(reviewContent);
|
|
134
|
+
const reviewTree = extractReviewedSnapshotTree(reviewContent);
|
|
135
|
+
if (!summary.ok || summary.summary.verdict !== 'Approved' || !commitExists(repoRoot, reviewBaseline) || !SHA_RE.test(reviewTree)) {
|
|
136
|
+
return invalid('authoritative review-code evidence is not a valid approved snapshot', currentHead, 'COMMIT_FINALIZATION_EVIDENCE_INVALID', shared);
|
|
137
|
+
}
|
|
138
|
+
const committedTree = git(repoRoot, ['rev-parse', `${intent.committedHead}^{tree}`]);
|
|
139
|
+
if (reviewBaseline !== intent.baselineHead || reviewTree !== committedTree) {
|
|
140
|
+
return outcome('conflict', {
|
|
141
|
+
...shared,
|
|
142
|
+
code: 'COMMIT_FINALIZATION_CONFLICT',
|
|
143
|
+
message: 'review baseline or snapshot tree conflicts with the committed intent'
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
const anchor = String(metadata.last_reviewed_commit ?? '').trim();
|
|
147
|
+
if (anchor && anchor !== intent.committedHead) {
|
|
148
|
+
return outcome('conflict', {
|
|
149
|
+
...shared,
|
|
150
|
+
code: 'COMMIT_FINALIZATION_CONFLICT',
|
|
151
|
+
message: 'task review anchor conflicts with the committed intent'
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
needsAnchor = anchor === '';
|
|
155
|
+
}
|
|
156
|
+
const commitNote = git(repoRoot, ['show', '-s', '--format=%h%x20%s', intent.committedHead]);
|
|
157
|
+
const matchingDone = commitRows.filter((row) => row.done !== '' && row.note === commitNote);
|
|
158
|
+
const needsLog = matchingDone.length === 0 && openRows.length === 1;
|
|
159
|
+
if (!(needsLog || (matchingDone.length === 1 && openRows.length === 0))) {
|
|
160
|
+
return outcome('conflict', {
|
|
161
|
+
...shared,
|
|
162
|
+
code: 'COMMIT_FINALIZATION_CONFLICT',
|
|
163
|
+
message: 'Commit activity identity is missing, duplicated, or ambiguous',
|
|
164
|
+
commitNote
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
return outcome('recoverable', {
|
|
168
|
+
...shared,
|
|
169
|
+
message: 'commit finalization can be recovered',
|
|
170
|
+
commitNote,
|
|
171
|
+
needsAnchor,
|
|
172
|
+
needsLog
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
function planCommitTaskFinalization(taskDir, inspection, agent, timestamp) {
|
|
176
|
+
if (inspection.disposition !== 'recoverable' || !inspection.committedHead || !inspection.commitNote) {
|
|
177
|
+
throw new Error('commit finalization task mutations require a recoverable inspection');
|
|
178
|
+
}
|
|
179
|
+
const taskContent = fs.readFileSync(path.join(taskDir, 'task.md'), 'utf8');
|
|
180
|
+
const frontmatter = { assigned_to: agent };
|
|
181
|
+
if (inspection.needsAnchor)
|
|
182
|
+
frontmatter.last_reviewed_commit = inspection.committedHead;
|
|
183
|
+
const mutations = [{
|
|
184
|
+
kind: 'frontmatter',
|
|
185
|
+
set: frontmatter
|
|
186
|
+
}];
|
|
187
|
+
if (inspection.needsLog) {
|
|
188
|
+
const activity = locateActivityLog(taskContent);
|
|
189
|
+
if (!activity)
|
|
190
|
+
throw new Error('task activity log is missing or ambiguous');
|
|
191
|
+
mutations.push({
|
|
192
|
+
kind: 'section',
|
|
193
|
+
aliases: ['活动日志', 'Activity Log'],
|
|
194
|
+
heading: activity.heading,
|
|
195
|
+
body: appendActivityEntry(activity, {
|
|
196
|
+
time: timestamp,
|
|
197
|
+
step: 'Commit',
|
|
198
|
+
agent,
|
|
199
|
+
note: inspection.commitNote
|
|
200
|
+
})
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
return { mutations, commitNote: inspection.commitNote };
|
|
204
|
+
}
|
|
205
|
+
function inspectCreatePrCommitGate(taskDir, repoRoot, taskId) {
|
|
206
|
+
const lifecycle = inspectCommitFinalization(taskDir, repoRoot, taskId);
|
|
207
|
+
if (lifecycle.disposition !== 'idle') {
|
|
208
|
+
const pending = lifecycle.disposition === 'prepared' || lifecycle.disposition === 'recoverable';
|
|
209
|
+
return {
|
|
210
|
+
allowed: false,
|
|
211
|
+
code: pending ? 'COMMIT_FINALIZATION_PENDING' : lifecycle.code ?? 'COMMIT_FINALIZATION_CONFLICT',
|
|
212
|
+
message: pending ? 'commit finalization must complete before creating a pull request' : lifecycle.message,
|
|
213
|
+
disposition: lifecycle.disposition,
|
|
214
|
+
action: 'rerun-commit'
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
const review = findAuthoritativeReviewCodeArtifact(taskDir);
|
|
218
|
+
if (!review.ok || !review.path) {
|
|
219
|
+
return { allowed: true, code: null, message: 'no review-code history requires an anchor', disposition: 'idle', action: 'continue' };
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
const content = fs.readFileSync(review.path, 'utf8');
|
|
223
|
+
const summary = parseReviewSummary(content);
|
|
224
|
+
if (!summary.ok || summary.summary.verdict !== 'Approved') {
|
|
225
|
+
return {
|
|
226
|
+
allowed: false,
|
|
227
|
+
code: 'COMMIT_FINALIZATION_EVIDENCE_INVALID',
|
|
228
|
+
message: 'latest authoritative review-code is not approved',
|
|
229
|
+
disposition: 'idle',
|
|
230
|
+
action: 'rerun-review-code'
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
const task = parseTypedTaskFrontmatter(fs.readFileSync(path.join(taskDir, 'task.md'), 'utf8'));
|
|
234
|
+
const currentHead = git(repoRoot, ['rev-parse', 'HEAD']);
|
|
235
|
+
const anchor = String(task.last_reviewed_commit ?? '').trim();
|
|
236
|
+
if (!anchor || !commitExists(repoRoot, anchor)) {
|
|
237
|
+
return {
|
|
238
|
+
allowed: false,
|
|
239
|
+
code: 'COMMIT_FINALIZATION_EVIDENCE_MISSING',
|
|
240
|
+
message: 'approved review snapshot is not anchored to a Git commit',
|
|
241
|
+
disposition: 'idle',
|
|
242
|
+
action: 'rerun-commit'
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
if (anchor !== currentHead || extractReviewedSnapshotTree(content) !== git(repoRoot, ['rev-parse', 'HEAD^{tree}'])) {
|
|
246
|
+
return {
|
|
247
|
+
allowed: false,
|
|
248
|
+
code: 'COMMIT_FINALIZATION_CONFLICT',
|
|
249
|
+
message: 'approved review anchor or tree does not match repository HEAD',
|
|
250
|
+
disposition: 'idle',
|
|
251
|
+
action: 'rerun-review-code'
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
return { allowed: true, code: null, message: 'commit finalization evidence is complete', disposition: 'idle', action: 'continue' };
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
return {
|
|
258
|
+
allowed: false,
|
|
259
|
+
code: 'COMMIT_FINALIZATION_EVIDENCE_INVALID',
|
|
260
|
+
message: error instanceof Error ? error.message : String(error),
|
|
261
|
+
disposition: 'idle',
|
|
262
|
+
action: 'rerun-review-code'
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
export { inspectCommitFinalization, inspectCreatePrCommitGate, planCommitTaskFinalization };
|
|
267
|
+
//# sourceMappingURL=commit-finalization.js.map
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { createHash, randomBytes, randomUUID, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
class CommitIntentError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
constructor(code, message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = 'CommitIntentError';
|
|
9
|
+
this.code = code;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function commitIntentPath(taskDir) {
|
|
13
|
+
return path.join(taskDir, 'commit-intent.json');
|
|
14
|
+
}
|
|
15
|
+
function digest(value) {
|
|
16
|
+
return createHash('sha256').update(value).digest('hex');
|
|
17
|
+
}
|
|
18
|
+
function serialize(value) {
|
|
19
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
20
|
+
}
|
|
21
|
+
function atomicWrite(file, value) {
|
|
22
|
+
const temp = `${file}.${process.pid}.${randomUUID()}.tmp`;
|
|
23
|
+
try {
|
|
24
|
+
fs.writeFileSync(temp, serialize(value), { encoding: 'utf8', flag: 'wx', mode: 0o600 });
|
|
25
|
+
const handle = fs.openSync(temp, 'r+');
|
|
26
|
+
try {
|
|
27
|
+
fs.fsyncSync(handle);
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
fs.closeSync(handle);
|
|
31
|
+
}
|
|
32
|
+
fs.renameSync(temp, file);
|
|
33
|
+
fs.chmodSync(file, 0o600);
|
|
34
|
+
try {
|
|
35
|
+
const directory = fs.openSync(path.dirname(file), 'r');
|
|
36
|
+
try {
|
|
37
|
+
fs.fsyncSync(directory);
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
fs.closeSync(directory);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
const code = error.code;
|
|
45
|
+
if (!['EACCES', 'EISDIR', 'EINVAL', 'EPERM'].includes(code ?? ''))
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
if (fs.existsSync(temp))
|
|
51
|
+
fs.unlinkSync(temp);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function isRecord(value) {
|
|
55
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
56
|
+
}
|
|
57
|
+
function keysEqual(value, expected) {
|
|
58
|
+
return Object.keys(value).sort().join('\0') === [...expected].sort().join('\0');
|
|
59
|
+
}
|
|
60
|
+
function validPushEvidence(value) {
|
|
61
|
+
return value === null || (isRecord(value)
|
|
62
|
+
&& keysEqual(value, ['remote', 'ref', 'head'])
|
|
63
|
+
&& ['remote', 'ref', 'head'].every((key) => typeof value[key] === 'string' && value[key] !== ''));
|
|
64
|
+
}
|
|
65
|
+
function validOrchestration(value) {
|
|
66
|
+
if (value === null)
|
|
67
|
+
return true;
|
|
68
|
+
if (!isRecord(value) || !keysEqual(value, [
|
|
69
|
+
'runId', 'receiptId', 'authorizationIssuedAt', 'sourceRunDigest', 'plannedRunDigest',
|
|
70
|
+
'completionUpdatedAt', 'plannedReceipt'
|
|
71
|
+
]))
|
|
72
|
+
return false;
|
|
73
|
+
return [
|
|
74
|
+
'runId', 'receiptId', 'authorizationIssuedAt', 'sourceRunDigest', 'plannedRunDigest', 'completionUpdatedAt'
|
|
75
|
+
].every((key) => typeof value[key] === 'string' && value[key] !== '') && isRecord(value.plannedReceipt);
|
|
76
|
+
}
|
|
77
|
+
function parseIntent(raw, taskId) {
|
|
78
|
+
let value;
|
|
79
|
+
try {
|
|
80
|
+
value = JSON.parse(raw);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
throw new CommitIntentError('ORCHESTRATION_COMMIT_INTENT_INVALID', 'commit intent is not valid JSON');
|
|
84
|
+
}
|
|
85
|
+
if (!isRecord(value) || !keysEqual(value, [
|
|
86
|
+
'schemaVersion', 'taskId', 'mode', 'phase', 'tokenDigest', 'baselineHead', 'committedHead',
|
|
87
|
+
'pushEvidence', 'orchestration', 'createdAt', 'updatedAt'
|
|
88
|
+
])) {
|
|
89
|
+
throw new CommitIntentError('ORCHESTRATION_COMMIT_INTENT_INVALID', 'commit intent has an invalid schema');
|
|
90
|
+
}
|
|
91
|
+
if (value.schemaVersion !== 1
|
|
92
|
+
|| value.taskId !== taskId
|
|
93
|
+
|| !['standalone', 'orchestrated'].includes(String(value.mode))
|
|
94
|
+
|| !['prepared', 'committed', 'pushed'].includes(String(value.phase))
|
|
95
|
+
|| typeof value.tokenDigest !== 'string'
|
|
96
|
+
|| !/^[a-f0-9]{64}$/.test(value.tokenDigest)
|
|
97
|
+
|| typeof value.baselineHead !== 'string'
|
|
98
|
+
|| (value.committedHead !== null && typeof value.committedHead !== 'string')
|
|
99
|
+
|| !validPushEvidence(value.pushEvidence)
|
|
100
|
+
|| !validOrchestration(value.orchestration)
|
|
101
|
+
|| typeof value.createdAt !== 'string'
|
|
102
|
+
|| typeof value.updatedAt !== 'string'
|
|
103
|
+
|| (value.mode === 'standalone') !== (value.orchestration === null)) {
|
|
104
|
+
throw new CommitIntentError('ORCHESTRATION_COMMIT_INTENT_INVALID', 'commit intent has invalid field values');
|
|
105
|
+
}
|
|
106
|
+
return value;
|
|
107
|
+
}
|
|
108
|
+
function tokenMatches(actualDigest, token) {
|
|
109
|
+
const actual = Buffer.from(actualDigest, 'hex');
|
|
110
|
+
const supplied = Buffer.from(digest(token), 'hex');
|
|
111
|
+
return actual.length === supplied.length && timingSafeEqual(actual, supplied);
|
|
112
|
+
}
|
|
113
|
+
function readCommitIntent(taskDir, taskId, token) {
|
|
114
|
+
const file = commitIntentPath(taskDir);
|
|
115
|
+
const intent = parseIntent(fs.readFileSync(file, 'utf8'), taskId);
|
|
116
|
+
if (token !== undefined && !tokenMatches(intent.tokenDigest, token)) {
|
|
117
|
+
throw new CommitIntentError('ORCHESTRATION_COMMIT_INTENT_TOKEN_MISMATCH', 'commit intent token does not match');
|
|
118
|
+
}
|
|
119
|
+
return intent;
|
|
120
|
+
}
|
|
121
|
+
function createCommitIntent(taskDir, input, options = {}) {
|
|
122
|
+
const file = commitIntentPath(taskDir);
|
|
123
|
+
if (fs.existsSync(file)) {
|
|
124
|
+
throw new CommitIntentError('ORCHESTRATION_COMMIT_INTENT_BUSY', 'an active commit intent already exists');
|
|
125
|
+
}
|
|
126
|
+
const token = (options.token ?? (() => randomBytes(32).toString('base64url')))();
|
|
127
|
+
const intent = Object.freeze({ schemaVersion: 1, ...input, tokenDigest: digest(token) });
|
|
128
|
+
atomicWrite(file, intent);
|
|
129
|
+
return { intent, token };
|
|
130
|
+
}
|
|
131
|
+
function updateCommitIntent(taskDir, taskId, token, updates) {
|
|
132
|
+
const current = readCommitIntent(taskDir, taskId, token);
|
|
133
|
+
const nextPhase = updates.phase ?? current.phase;
|
|
134
|
+
const transitions = {
|
|
135
|
+
prepared: ['prepared', 'committed', 'pushed'],
|
|
136
|
+
committed: ['committed', 'pushed'],
|
|
137
|
+
pushed: ['pushed']
|
|
138
|
+
};
|
|
139
|
+
if (!transitions[current.phase].includes(nextPhase)) {
|
|
140
|
+
throw new CommitIntentError('ORCHESTRATION_COMMIT_INTENT_STATE_INVALID', `cannot move commit intent from ${current.phase} to ${nextPhase}`);
|
|
141
|
+
}
|
|
142
|
+
const updated = Object.freeze({ ...current, ...updates, phase: nextPhase });
|
|
143
|
+
atomicWrite(commitIntentPath(taskDir), updated);
|
|
144
|
+
return updated;
|
|
145
|
+
}
|
|
146
|
+
function removeCommitIntent(taskDir, taskId, token) {
|
|
147
|
+
readCommitIntent(taskDir, taskId, token);
|
|
148
|
+
fs.unlinkSync(commitIntentPath(taskDir));
|
|
149
|
+
}
|
|
150
|
+
function removeCommitIntentByDigest(taskDir, taskId, expectedDigest) {
|
|
151
|
+
const file = commitIntentPath(taskDir);
|
|
152
|
+
const raw = fs.readFileSync(file);
|
|
153
|
+
parseIntent(raw.toString('utf8'), taskId);
|
|
154
|
+
const actual = digest(raw);
|
|
155
|
+
const expected = Buffer.from(expectedDigest, 'hex');
|
|
156
|
+
const observed = Buffer.from(actual, 'hex');
|
|
157
|
+
if (!/^[a-f0-9]{64}$/.test(expectedDigest)
|
|
158
|
+
|| expected.length !== observed.length
|
|
159
|
+
|| !timingSafeEqual(expected, observed)) {
|
|
160
|
+
throw new CommitIntentError('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'commit intent changed after recovery inspection');
|
|
161
|
+
}
|
|
162
|
+
fs.unlinkSync(file);
|
|
163
|
+
}
|
|
164
|
+
export { CommitIntentError, commitIntentPath, createCommitIntent, digest, readCommitIntent, removeCommitIntent, removeCommitIntentByDigest, serialize, updateCommitIntent };
|
|
165
|
+
//# sourceMappingURL=commit-intent.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import { appendActivityEntry, locateActivityLog } from './activity-log.js';
|
|
3
3
|
import { isDecisionItem, listDecisionItems, selectDecisionItem } from './decision-items.js';
|
|
4
|
-
import { createImplementationInput, IMPLEMENTATION_INPUT_ALIASES, parseImplementationInputs, renderImplementationInputs } from './implementation-inputs.js';
|
|
4
|
+
import { createImplementationInput, finalizeImplementationInput, IMPLEMENTATION_INPUT_ALIASES, parseImplementationInputs, renderImplementationInputs, selectDeclaredImplementationInput } from './implementation-inputs.js';
|
|
5
5
|
import { LEDGER_COLUMNS, LEDGER_HEADINGS, parseLedger } from './ledger.js';
|
|
6
6
|
import { resolveTaskContext } from './resolve-ref.js';
|
|
7
7
|
import { extractSection, extractSubSection, findSectionHeading } from './sections.js';
|
|
@@ -45,7 +45,7 @@ function existingDecision(content, row, decision, needsImplementation) {
|
|
|
45
45
|
? { recordId, implementationInputId: null }
|
|
46
46
|
: { conflict: '--needs-implementation is only valid for code-stage decisions' };
|
|
47
47
|
const inputs = parseImplementationInputs(content).rows.filter((input) => input.ledgerId === row.id && input.decisionEvidence === row.evidence);
|
|
48
|
-
if (inputs.length !== 1 || inputs[0].needsImplementation !== needsImplementation) {
|
|
48
|
+
if (inputs.length !== 1 || (needsImplementation !== undefined && inputs[0].needsImplementation !== needsImplementation)) {
|
|
49
49
|
return { conflict: `implementation intent for '${row.id}' conflicts with the existing decision` };
|
|
50
50
|
}
|
|
51
51
|
return { recordId, implementationInputId: inputs[0].id };
|
|
@@ -87,10 +87,29 @@ function applyHumanDecision(request, options = {}) {
|
|
|
87
87
|
return failed('DECISION_TARGET_INVALID', message, resolved.taskId);
|
|
88
88
|
}
|
|
89
89
|
const row = selected.row;
|
|
90
|
-
if (row.stage === 'code' && request.needsImplementation === undefined)
|
|
91
|
-
return failed('DECISION_PAYLOAD_INVALID', 'code-stage decisions require --needs-implementation true|false', resolved.taskId, row.id);
|
|
92
90
|
if (row.stage !== 'code' && request.needsImplementation !== undefined)
|
|
93
91
|
return failed('DECISION_PAYLOAD_INVALID', '--needs-implementation is only valid for code-stage decisions', resolved.taskId, row.id);
|
|
92
|
+
let parsedInputs = null;
|
|
93
|
+
let declaredInput = null;
|
|
94
|
+
let effectiveNeedsImplementation = request.needsImplementation;
|
|
95
|
+
if (row.stage === 'code') {
|
|
96
|
+
try {
|
|
97
|
+
parsedInputs = parseImplementationInputs(content);
|
|
98
|
+
declaredInput = selectDeclaredImplementationInput(parsedInputs.rows, row.id, row.evidence);
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
return failed('DECISION_DOCUMENT_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId, row.id);
|
|
102
|
+
}
|
|
103
|
+
if (declaredInput) {
|
|
104
|
+
if (request.needsImplementation !== undefined && request.needsImplementation !== declaredInput.needsImplementation) {
|
|
105
|
+
return failed('DECISION_CONFLICT', `implementation intent for '${row.id}' conflicts with its declaration`, resolved.taskId, row.id);
|
|
106
|
+
}
|
|
107
|
+
effectiveNeedsImplementation = declaredInput.needsImplementation;
|
|
108
|
+
}
|
|
109
|
+
else if (effectiveNeedsImplementation === undefined) {
|
|
110
|
+
return failed('DECISION_PAYLOAD_INVALID', 'code-stage decisions require --needs-implementation true|false when no implementation intent was declared', resolved.taskId, row.id);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
94
113
|
let metadata;
|
|
95
114
|
try {
|
|
96
115
|
metadata = (options.metadataProvider ?? captureTaskWriteMetadata)();
|
|
@@ -119,22 +138,17 @@ function applyHumanDecision(request, options = {}) {
|
|
|
119
138
|
];
|
|
120
139
|
let implementationInputId = null;
|
|
121
140
|
if (row.stage === 'code') {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const input = createImplementationInput(parsed.rows, {
|
|
130
|
-
ledgerId: row.id, decisionEvidence: evidence,
|
|
131
|
-
needsImplementation: request.needsImplementation, decidedAt: metadata.timestamp
|
|
132
|
-
});
|
|
133
|
-
implementationInputId = input.id;
|
|
141
|
+
const nextInputs = declaredInput
|
|
142
|
+
? finalizeImplementationInput(parsedInputs.rows, declaredInput.id, evidence, metadata.timestamp)
|
|
143
|
+
: [...parsedInputs.rows, createImplementationInput(parsedInputs.rows, {
|
|
144
|
+
ledgerId: row.id, decisionEvidence: evidence,
|
|
145
|
+
needsImplementation: effectiveNeedsImplementation, decidedAt: metadata.timestamp
|
|
146
|
+
})];
|
|
147
|
+
implementationInputId = declaredInput?.id ?? nextInputs[nextInputs.length - 1].id;
|
|
134
148
|
mutations.push({
|
|
135
149
|
kind: 'section', aliases: IMPLEMENTATION_INPUT_ALIASES,
|
|
136
150
|
heading: findSectionHeading(content, [...IMPLEMENTATION_INPUT_ALIASES]),
|
|
137
|
-
body: renderImplementationInputs(
|
|
151
|
+
body: renderImplementationInputs(nextInputs)
|
|
138
152
|
});
|
|
139
153
|
}
|
|
140
154
|
mutations.push({
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { normalizeAgentToken } from '../agent-clients/tokens.js';
|
|
2
3
|
const MANAGED_AGENTS = {
|
|
3
4
|
'agent-infra-lifecycle-executor': 'executor',
|
|
4
5
|
'agent-infra-lifecycle-reviewer': 'reviewer'
|
|
@@ -83,8 +84,7 @@ function completeDelegationStage(receipt, event) {
|
|
|
83
84
|
if (event.stage !== receipt.stage || event.round !== receipt.round || event.artifact !== receipt.artifact) {
|
|
84
85
|
return fail('DELEGATION_STAGE_MISMATCH', 'stage completion identity does not match the active delegation');
|
|
85
86
|
}
|
|
86
|
-
|
|
87
|
-
if (!acceptedAgents.includes(event.agent)) {
|
|
87
|
+
if (normalizeAgentToken(event.agent) !== normalizeAgentToken(receipt.client)) {
|
|
88
88
|
return fail('DELEGATION_AGENT_MISMATCH', `stage agent '${event.agent}' does not match client '${receipt.client}'`);
|
|
89
89
|
}
|
|
90
90
|
return { ok: true, receipt: Object.freeze({ ...receipt, status: 'stage-completed', agent: event.agent }) };
|
|
@@ -13,12 +13,11 @@ function validateInput(row) {
|
|
|
13
13
|
throw new Error(`implementation input ${row.id} has incomplete decision identity`);
|
|
14
14
|
if (row.stage !== 'code')
|
|
15
15
|
throw new Error(`implementation input ${row.id} has invalid stage`);
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
(
|
|
21
|
-
(row.needsImplementation && row.status === 'consumed' && ARTIFACT_RE.test(row.consumedBy));
|
|
16
|
+
const hasValidTimestamp = TIMESTAMP_RE.test(row.decidedAt) && Number.isFinite(Date.parse(row.decidedAt.replace(' ', 'T')));
|
|
17
|
+
const valid = (row.status === 'declared' && row.decidedAt === '' && row.consumedBy === '') ||
|
|
18
|
+
(hasValidTimestamp && row.needsImplementation && row.status === 'pending' && row.consumedBy === '') ||
|
|
19
|
+
(hasValidTimestamp && !row.needsImplementation && row.status === 'not-required' && row.consumedBy === '') ||
|
|
20
|
+
(hasValidTimestamp && row.needsImplementation && row.status === 'consumed' && ARTIFACT_RE.test(row.consumedBy));
|
|
22
21
|
if (!valid)
|
|
23
22
|
throw new Error(`implementation input ${row.id} has an invalid state combination`);
|
|
24
23
|
}
|
|
@@ -67,6 +66,54 @@ function createImplementationInput(rows, draft) {
|
|
|
67
66
|
validateInput(row);
|
|
68
67
|
return row;
|
|
69
68
|
}
|
|
69
|
+
function declareImplementationInput(rows, draft) {
|
|
70
|
+
const existing = rows.filter((row) => row.ledgerId === draft.ledgerId && row.status === 'declared');
|
|
71
|
+
if (existing.length > 1)
|
|
72
|
+
throw new Error(`implementation input declaration for '${draft.ledgerId}' conflicts with existing rows`);
|
|
73
|
+
if (existing.length === 1) {
|
|
74
|
+
const row = existing[0];
|
|
75
|
+
if (row.decisionEvidence !== draft.decisionEvidence || row.needsImplementation !== draft.needsImplementation) {
|
|
76
|
+
throw new Error(`implementation input declaration for '${draft.ledgerId}' conflicts with the existing row`);
|
|
77
|
+
}
|
|
78
|
+
return row;
|
|
79
|
+
}
|
|
80
|
+
const row = {
|
|
81
|
+
id: nextId(rows), ledgerId: draft.ledgerId, decisionEvidence: draft.decisionEvidence,
|
|
82
|
+
stage: 'code', needsImplementation: draft.needsImplementation, decidedAt: '',
|
|
83
|
+
status: 'declared', consumedBy: ''
|
|
84
|
+
};
|
|
85
|
+
validateInput(row);
|
|
86
|
+
return row;
|
|
87
|
+
}
|
|
88
|
+
function selectDeclaredImplementationInput(rows, ledgerId, decisionEvidence) {
|
|
89
|
+
const forLedger = rows.filter((row) => row.ledgerId === ledgerId && row.status === 'declared');
|
|
90
|
+
if (forLedger.length > 1)
|
|
91
|
+
throw new Error(`implementation input declaration for '${ledgerId}' is ambiguous`);
|
|
92
|
+
if (forLedger.length === 0)
|
|
93
|
+
return null;
|
|
94
|
+
if (forLedger[0].decisionEvidence !== decisionEvidence) {
|
|
95
|
+
throw new Error(`implementation input declaration for '${ledgerId}' has conflicting evidence`);
|
|
96
|
+
}
|
|
97
|
+
return forLedger[0];
|
|
98
|
+
}
|
|
99
|
+
function finalizeImplementationInput(rows, id, decisionEvidence, decidedAt) {
|
|
100
|
+
let found = false;
|
|
101
|
+
const next = rows.map((row) => {
|
|
102
|
+
if (row.id !== id)
|
|
103
|
+
return row;
|
|
104
|
+
found = true;
|
|
105
|
+
if (row.status !== 'declared')
|
|
106
|
+
throw new Error(`implementation input ${id} is not declared`);
|
|
107
|
+
return {
|
|
108
|
+
...row, decisionEvidence, decidedAt,
|
|
109
|
+
status: row.needsImplementation ? 'pending' : 'not-required'
|
|
110
|
+
};
|
|
111
|
+
});
|
|
112
|
+
if (!found)
|
|
113
|
+
throw new Error(`implementation input ${id} was not found`);
|
|
114
|
+
next.forEach(validateInput);
|
|
115
|
+
return next;
|
|
116
|
+
}
|
|
70
117
|
function selectPendingImplementationInput(rows, reviewCompletedAt) {
|
|
71
118
|
const reviewEpoch = Date.parse(reviewCompletedAt.replace(' ', 'T'));
|
|
72
119
|
if (!Number.isFinite(reviewEpoch))
|
|
@@ -113,5 +160,5 @@ function renderImplementationInputs(rows) {
|
|
|
113
160
|
})
|
|
114
161
|
].join('\n');
|
|
115
162
|
}
|
|
116
|
-
export { SECTION_ALIASES as IMPLEMENTATION_INPUT_ALIASES, createImplementationInput, consumeImplementationInput, parseImplementationInputs, renderImplementationInputs, selectPendingImplementationInput };
|
|
163
|
+
export { SECTION_ALIASES as IMPLEMENTATION_INPUT_ALIASES, createImplementationInput, consumeImplementationInput, declareImplementationInput, finalizeImplementationInput, parseImplementationInputs, renderImplementationInputs, selectDeclaredImplementationInput, selectPendingImplementationInput };
|
|
117
164
|
//# sourceMappingURL=implementation-inputs.js.map
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { declareImplementationInput, IMPLEMENTATION_INPUT_ALIASES, parseImplementationInputs, renderImplementationInputs } from './implementation-inputs.js';
|
|
3
4
|
import { LEDGER_COLUMNS, LEDGER_HEADINGS, nextHdId, parseLedger, validateLedgerRows } from './ledger.js';
|
|
4
5
|
import { resolveTaskRef } from './resolve-ref.js';
|
|
5
6
|
import { writeTask } from './write.js';
|
|
@@ -66,6 +67,14 @@ function ledgerSectionMutation(content) {
|
|
|
66
67
|
body: `| ${LEDGER_COLUMNS.join(' | ')} |\n|----|-------|-------|----------|--------|----------|`
|
|
67
68
|
}];
|
|
68
69
|
}
|
|
70
|
+
function implementationInputMutation(content, rows) {
|
|
71
|
+
const english = /^##\s+Activity Log\s*$/m.test(content);
|
|
72
|
+
return {
|
|
73
|
+
kind: 'section', aliases: IMPLEMENTATION_INPUT_ALIASES,
|
|
74
|
+
heading: english ? 'Implementation Inputs' : '实现输入',
|
|
75
|
+
body: renderImplementationInputs(rows)
|
|
76
|
+
};
|
|
77
|
+
}
|
|
69
78
|
function mapWrite(intent, entityId, before, after, result) {
|
|
70
79
|
if (result.status === 'failed')
|
|
71
80
|
return failed(intent, result.error.code, result.error.message, result.taskId, entityId);
|
|
@@ -174,9 +183,34 @@ function applyLedgerIntent(intent, options = {}) {
|
|
|
174
183
|
}
|
|
175
184
|
}
|
|
176
185
|
}
|
|
186
|
+
let implementationMutation = null;
|
|
187
|
+
if (intent.kind === 'decision-upsert' || intent.kind === 'finding-review') {
|
|
188
|
+
const stage = intent.kind === 'decision-upsert' ? intent.stage : after.stage;
|
|
189
|
+
const escalates = intent.kind === 'decision-upsert' || intent.status === 'needs-human-decision';
|
|
190
|
+
if (stage === 'code' && escalates && intent.needsImplementation === undefined) {
|
|
191
|
+
return failed(intent, 'LEDGER_PAYLOAD_INVALID', 'code-stage decisions require --needs-implementation true|false', resolved.taskId, after.id);
|
|
192
|
+
}
|
|
193
|
+
if ((stage !== 'code' || !escalates) && intent.needsImplementation !== undefined) {
|
|
194
|
+
return failed(intent, 'LEDGER_PAYLOAD_INVALID', '--needs-implementation is only valid for code-stage escalation', resolved.taskId, after.id);
|
|
195
|
+
}
|
|
196
|
+
if (stage === 'code' && escalates) {
|
|
197
|
+
try {
|
|
198
|
+
const parsed = parseImplementationInputs(content);
|
|
199
|
+
const declaration = declareImplementationInput(parsed.rows, {
|
|
200
|
+
ledgerId: after.id, decisionEvidence: after.evidence,
|
|
201
|
+
needsImplementation: intent.needsImplementation
|
|
202
|
+
});
|
|
203
|
+
const nextRows = parsed.rows.includes(declaration) ? parsed.rows : [...parsed.rows, declaration];
|
|
204
|
+
implementationMutation = implementationInputMutation(content, nextRows);
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
return failed(intent, 'LEDGER_DOCUMENT_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId, after.id);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
177
211
|
const result = writeTask({
|
|
178
212
|
taskRef: intent.taskRef, expectedState: 'active',
|
|
179
|
-
mutations: [...ledgerSectionMutation(content), rowMutation(after)],
|
|
213
|
+
mutations: [...ledgerSectionMutation(content), rowMutation(after), ...(implementationMutation ? [implementationMutation] : [])],
|
|
180
214
|
dryRun: 'dryRun' in intent ? intent.dryRun : false
|
|
181
215
|
}, { ...options, taskLocation: { repoRoot: resolved.repoRoot, taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, state: resolved.state } });
|
|
182
216
|
return mapWrite(intent, after.id, before, after, result);
|