@fitlab-ai/agent-infra 0.9.3 → 0.9.5
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 +3 -1
- package/README.zh-CN.md +3 -1
- package/bin/cli.ts +12 -0
- package/dist/bin/cli.js +14 -0
- package/dist/bin/internal-cli.js +144 -123
- package/dist/lib/agent-clients/adapter.js +94 -4
- 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/codex-lifecycle/app-server.js +382 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +163 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +211 -0
- package/dist/lib/agent-clients/adapters/codex.js +2 -2
- 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/git/worktree-identity.js +64 -0
- package/dist/lib/internal/codex-lifecycle.js +202 -0
- package/dist/lib/internal/sandbox-control.js +24 -0
- package/dist/lib/internal/task-activity.js +45 -13
- package/dist/lib/internal/task-create.js +48 -0
- 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 +170 -27
- 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 +110 -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 +58 -0
- package/dist/lib/sandbox/control/protocol.js +54 -0
- package/dist/lib/sandbox/control/server.js +144 -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 +109 -0
- package/dist/lib/task/activity-intent.js +257 -51
- package/dist/lib/task/activity-log.js +32 -5
- package/dist/lib/task/commit-finalization.js +278 -0
- package/dist/lib/task/commit-intent.js +165 -0
- package/dist/lib/task/create-service.js +137 -0
- package/dist/lib/task/create.js +333 -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 +505 -34
- package/dist/lib/task/resolve-ref.js +41 -1
- package/dist/lib/task/review-fingerprint.js +4 -0
- package/dist/lib/task/workspace-snapshot.js +78 -3
- package/lib/agent-clients/adapter.ts +130 -6
- 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/codex-lifecycle/app-server.ts +442 -0
- package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +307 -0
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +250 -0
- package/lib/agent-clients/adapters/codex.ts +2 -2
- 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/git/worktree-identity.ts +86 -0
- package/lib/internal/codex-lifecycle.ts +204 -0
- package/lib/internal/sandbox-control.ts +24 -0
- package/lib/internal/task-activity.ts +50 -14
- package/lib/internal/task-create.ts +49 -0
- package/lib/internal/task-ledger.ts +12 -3
- package/lib/internal/task-lifecycle.ts +22 -1
- package/lib/internal/task-orchestration.ts +117 -21
- 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 +127 -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 +79 -0
- package/lib/sandbox/control/protocol.ts +110 -0
- package/lib/sandbox/control/server.ts +155 -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 +150 -0
- package/lib/task/activity-intent.ts +350 -61
- package/lib/task/activity-log.ts +46 -7
- package/lib/task/commit-finalization.ts +376 -0
- package/lib/task/commit-intent.ts +230 -0
- package/lib/task/create-service.ts +171 -0
- package/lib/task/create.ts +372 -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 +673 -41
- package/lib/task/resolve-ref.ts +46 -1
- package/lib/task/review-fingerprint.ts +9 -0
- package/lib/task/workspace-snapshot.ts +98 -4
- package/package.json +7 -7
- package/templates/.agents/hooks/lifecycle-delegation.js +47 -3
- 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 +3 -0
- package/templates/.agents/rules/task-management.zh-CN.md +3 -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 -17
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +28 -16
- package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +50 -0
- package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +56 -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/SKILL.en.md +26 -43
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +26 -43
- 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/.codex/hooks.json +50 -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,333 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { loadShortIdByTaskId, mutateShortIdRegistry } from './short-id.js';
|
|
5
|
+
import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.js';
|
|
6
|
+
const AGENTS = ['claude', 'codex', 'antigravity', 'opencode', 'cursor'];
|
|
7
|
+
const TYPES = ['feature', 'bugfix', 'refactor', 'docs', 'chore'];
|
|
8
|
+
const PRIORITIES = ['Urgent', 'High', 'Medium', 'Low'];
|
|
9
|
+
const EFFORTS = ['High', 'Medium', 'Low'];
|
|
10
|
+
const TASK_INPUT_KEYS = [
|
|
11
|
+
'sources', 'facts', 'constraints', 'decisions', 'alternatives',
|
|
12
|
+
'acceptanceCriteria', 'openQuestions'
|
|
13
|
+
];
|
|
14
|
+
function payloadError(message) {
|
|
15
|
+
throw new Error(`TASK_CREATE_PAYLOAD_INVALID: ${message}`);
|
|
16
|
+
}
|
|
17
|
+
function exactKeys(value, expected, label) {
|
|
18
|
+
const actual = Object.keys(value).sort();
|
|
19
|
+
const wanted = [...expected].sort();
|
|
20
|
+
if (actual.length !== wanted.length || actual.some((key, index) => key !== wanted[index])) {
|
|
21
|
+
payloadError(`${label} has unknown or missing fields`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function cleanString(value, label, maxBytes, oneLine = false) {
|
|
25
|
+
if (typeof value !== 'string' || value.length === 0 || Buffer.byteLength(value, 'utf8') > maxBytes) {
|
|
26
|
+
payloadError(`${label} must be a non-empty string within ${maxBytes} bytes`);
|
|
27
|
+
}
|
|
28
|
+
if (/\0|[\u0001-\u0008\u000b\u000c\u000e-\u001f\u007f]/u.test(value) || (oneLine && /[\r\n]/u.test(value))) {
|
|
29
|
+
payloadError(`${label} contains unsupported control characters`);
|
|
30
|
+
}
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
function enumValue(value, allowed, label) {
|
|
34
|
+
if (typeof value !== 'string' || !allowed.includes(value))
|
|
35
|
+
payloadError(`${label} is invalid`);
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
function validateTaskCreateCandidate(value) {
|
|
39
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
40
|
+
payloadError('candidate must be an object');
|
|
41
|
+
const candidate = value;
|
|
42
|
+
exactKeys(candidate, [
|
|
43
|
+
'version', 'idempotencyKey', 'agent', 'title', 'type', 'branchSlug',
|
|
44
|
+
'priority', 'effort', 'description', 'taskInput'
|
|
45
|
+
], 'candidate');
|
|
46
|
+
if (candidate.version !== 1)
|
|
47
|
+
payloadError('version must be 1');
|
|
48
|
+
const idempotencyKey = cleanString(candidate.idempotencyKey, 'idempotencyKey', 64, true);
|
|
49
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(idempotencyKey)) {
|
|
50
|
+
payloadError('idempotencyKey must be a UUID v4');
|
|
51
|
+
}
|
|
52
|
+
const title = cleanString(candidate.title, 'title', 400, true);
|
|
53
|
+
if ([...title].length > 100)
|
|
54
|
+
payloadError('title exceeds 100 Unicode code points');
|
|
55
|
+
const branchSlug = cleanString(candidate.branchSlug, 'branchSlug', 64, true);
|
|
56
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)+$/.test(branchSlug) || branchSlug.length < 3) {
|
|
57
|
+
payloadError('branchSlug must be lowercase ASCII kebab-case with length 3-64');
|
|
58
|
+
}
|
|
59
|
+
const description = cleanString(candidate.description, 'description', 8 * 1024);
|
|
60
|
+
if (!candidate.taskInput || typeof candidate.taskInput !== 'object' || Array.isArray(candidate.taskInput)) {
|
|
61
|
+
payloadError('taskInput must be an object');
|
|
62
|
+
}
|
|
63
|
+
const rawInput = candidate.taskInput;
|
|
64
|
+
exactKeys(rawInput, TASK_INPUT_KEYS, 'taskInput');
|
|
65
|
+
const taskInput = Object.fromEntries(TASK_INPUT_KEYS.map((key) => {
|
|
66
|
+
const list = rawInput[key];
|
|
67
|
+
if (!Array.isArray(list) || list.length > 50)
|
|
68
|
+
payloadError(`taskInput.${key} must contain at most 50 items`);
|
|
69
|
+
return [key, list.map((item, index) => cleanString(item, `taskInput.${key}[${index}]`, 2 * 1024, true))];
|
|
70
|
+
}));
|
|
71
|
+
return {
|
|
72
|
+
version: 1,
|
|
73
|
+
idempotencyKey,
|
|
74
|
+
agent: enumValue(candidate.agent, AGENTS, 'agent'),
|
|
75
|
+
title,
|
|
76
|
+
type: enumValue(candidate.type, TYPES, 'type'),
|
|
77
|
+
branchSlug,
|
|
78
|
+
priority: enumValue(candidate.priority, PRIORITIES, 'priority'),
|
|
79
|
+
effort: enumValue(candidate.effort, EFFORTS, 'effort'),
|
|
80
|
+
description,
|
|
81
|
+
taskInput
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function canonicalTaskCreateCandidate(candidate) {
|
|
85
|
+
const valid = validateTaskCreateCandidate(candidate);
|
|
86
|
+
return JSON.stringify({
|
|
87
|
+
version: valid.version,
|
|
88
|
+
idempotencyKey: valid.idempotencyKey,
|
|
89
|
+
agent: valid.agent,
|
|
90
|
+
title: valid.title,
|
|
91
|
+
type: valid.type,
|
|
92
|
+
branchSlug: valid.branchSlug,
|
|
93
|
+
priority: valid.priority,
|
|
94
|
+
effort: valid.effort,
|
|
95
|
+
description: valid.description,
|
|
96
|
+
taskInput: Object.fromEntries(TASK_INPUT_KEYS.map((key) => [key, valid.taskInput[key]]))
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
function sha256(value) {
|
|
100
|
+
return createHash('sha256').update(value).digest('hex');
|
|
101
|
+
}
|
|
102
|
+
function formatTimestamp(date) {
|
|
103
|
+
const pad = (value) => String(value).padStart(2, '0');
|
|
104
|
+
const offsetMinutes = -date.getTimezoneOffset();
|
|
105
|
+
const sign = offsetMinutes >= 0 ? '+' : '-';
|
|
106
|
+
const offset = Math.abs(offsetMinutes);
|
|
107
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}${sign}${pad(Math.floor(offset / 60))}:${pad(offset % 60)}`;
|
|
108
|
+
}
|
|
109
|
+
function taskIdFor(date) {
|
|
110
|
+
const pad = (value) => String(value).padStart(2, '0');
|
|
111
|
+
return `TASK-${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}-${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`;
|
|
112
|
+
}
|
|
113
|
+
function yamlString(value) {
|
|
114
|
+
return JSON.stringify(value);
|
|
115
|
+
}
|
|
116
|
+
function markdownBlock(value) {
|
|
117
|
+
return value.split(/\r?\n/).map((line) => /^(?:#{1,6}\s|```)/.test(line) ? `\\${line}` : line).join('\n');
|
|
118
|
+
}
|
|
119
|
+
function renderList(items) {
|
|
120
|
+
return items.map((item) => `- ${item}`).join('\n');
|
|
121
|
+
}
|
|
122
|
+
function replaceEmptySubsection(content, heading, items) {
|
|
123
|
+
if (items.length === 0)
|
|
124
|
+
return content;
|
|
125
|
+
return content.replace(`${heading}\n`, `${heading}\n\n${renderList(items)}\n`);
|
|
126
|
+
}
|
|
127
|
+
function renderTask(params) {
|
|
128
|
+
const { candidate } = params;
|
|
129
|
+
const workflow = candidate.type === 'bugfix' ? 'bug-fix' : candidate.type === 'refactor' ? 'refactoring' : 'feature-development';
|
|
130
|
+
const branch = `${params.project}-${candidate.type}-${candidate.branchSlug}`;
|
|
131
|
+
const frontmatter = [
|
|
132
|
+
'---',
|
|
133
|
+
`id: ${params.taskId}`,
|
|
134
|
+
`type: ${candidate.type}`,
|
|
135
|
+
`branch: ${branch}`,
|
|
136
|
+
`workflow: ${workflow}`,
|
|
137
|
+
'status: active',
|
|
138
|
+
`created_at: ${params.timestamp}`,
|
|
139
|
+
`updated_at: ${params.timestamp}`,
|
|
140
|
+
`agent_infra_version: ${params.agentInfraVersion}`,
|
|
141
|
+
`priority: ${candidate.priority}`,
|
|
142
|
+
`effort: ${candidate.effort}`,
|
|
143
|
+
'start_date:',
|
|
144
|
+
'target_date:',
|
|
145
|
+
'current_step: requirement-analysis',
|
|
146
|
+
`assigned_to: ${candidate.agent}`,
|
|
147
|
+
'pr_status: pending',
|
|
148
|
+
`task_create_key_digest: ${params.keyDigest}`,
|
|
149
|
+
`task_create_candidate_digest: ${params.candidateDigest}`,
|
|
150
|
+
'---'
|
|
151
|
+
].join('\n');
|
|
152
|
+
let content = params.template.replace(/^---\n[\s\S]*?\n---/, frontmatter);
|
|
153
|
+
content = content.replace('# 任务:[标题]', `# 任务:${candidate.title}`);
|
|
154
|
+
content = content.replace('[清晰简洁地描述任务。]', markdownBlock(candidate.description));
|
|
155
|
+
const sections = [
|
|
156
|
+
['### 来源', 'sources'], ['### 已确认事实与证据', 'facts'], ['### 约束', 'constraints'],
|
|
157
|
+
['### 已确认决策', 'decisions'], ['### 候选与否决方案', 'alternatives'],
|
|
158
|
+
['### 验收标准', 'acceptanceCriteria'], ['### 未决事项', 'openQuestions']
|
|
159
|
+
];
|
|
160
|
+
for (const [heading, key] of sections)
|
|
161
|
+
content = replaceEmptySubsection(content, heading, candidate.taskInput[key]);
|
|
162
|
+
content = content.replace('- **关联 Issue**:#XXX', '- **关联 Issue**:N/A');
|
|
163
|
+
content = content.replace('- **关联 PR**:#XXX', '- **关联 PR**:N/A');
|
|
164
|
+
content = content.replace('- **分支**:`feature/xxx`', `- **分支**:\`${branch}\``);
|
|
165
|
+
const log = [
|
|
166
|
+
`- ${params.timestamp} — **Create Task [started]** by ${candidate.agent} — started`,
|
|
167
|
+
`- ${params.timestamp} — **Create Task** by ${candidate.agent} — Task created from structured candidate`
|
|
168
|
+
].join('\n');
|
|
169
|
+
content = content.replace('\n## 完成检查清单', `\n${log}\n\n## 完成检查清单`);
|
|
170
|
+
return `${content.trimEnd()}\n`;
|
|
171
|
+
}
|
|
172
|
+
function readProject(repoRoot) {
|
|
173
|
+
const config = JSON.parse(fs.readFileSync(path.join(repoRoot, '.agents', '.airc.json'), 'utf8'));
|
|
174
|
+
if (typeof config.project !== 'string' || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(config.project)) {
|
|
175
|
+
throw new Error('TASK_CREATE_CONFIG_INVALID: project is missing or invalid');
|
|
176
|
+
}
|
|
177
|
+
return config.project;
|
|
178
|
+
}
|
|
179
|
+
function assertRealDirectory(directory, code) {
|
|
180
|
+
const stat = fs.lstatSync(directory);
|
|
181
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
182
|
+
throw new Error(code);
|
|
183
|
+
}
|
|
184
|
+
function withCreateLock(repoRoot, workspaceRoot, operation) {
|
|
185
|
+
const lockRoot = path.join(workspaceRoot, '.task-create.lock');
|
|
186
|
+
fs.mkdirSync(lockRoot, { recursive: true, mode: 0o700 });
|
|
187
|
+
assertRealDirectory(lockRoot, 'TASK_CREATE_LOCK_FAILED');
|
|
188
|
+
let callbackError;
|
|
189
|
+
try {
|
|
190
|
+
return withTaskExecutionLock(repoRoot, 'task-create', 'task-create', () => {
|
|
191
|
+
try {
|
|
192
|
+
return operation();
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
callbackError = error;
|
|
196
|
+
throw error;
|
|
197
|
+
}
|
|
198
|
+
}, { lockRoot });
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
if (callbackError !== undefined)
|
|
202
|
+
throw callbackError;
|
|
203
|
+
if (error instanceof TaskExecutionLockError && error.code === 'ORCHESTRATION_LOCK_BUSY') {
|
|
204
|
+
throw new Error('TASK_CREATE_LOCK_TIMEOUT');
|
|
205
|
+
}
|
|
206
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
207
|
+
throw new Error(`TASK_CREATE_LOCK_FAILED: ${message}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
function writeReceipt(receiptPath, value) {
|
|
211
|
+
const temporary = `${receiptPath}.tmp.${process.pid}.${randomUUID()}`;
|
|
212
|
+
fs.writeFileSync(temporary, `${JSON.stringify(value)}\n`, { flag: 'wx', mode: 0o600 });
|
|
213
|
+
try {
|
|
214
|
+
fs.renameSync(temporary, receiptPath);
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
try {
|
|
218
|
+
fs.unlinkSync(temporary);
|
|
219
|
+
}
|
|
220
|
+
catch { /* best effort */ }
|
|
221
|
+
throw error;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
function recoverPublishedTask(activeRoot, repoRoot, keyDigest, candidateDigest) {
|
|
225
|
+
for (const name of fs.readdirSync(activeRoot)) {
|
|
226
|
+
if (!/^TASK-\d{8}-\d{6}$/.test(name))
|
|
227
|
+
continue;
|
|
228
|
+
const taskDir = path.join(activeRoot, name);
|
|
229
|
+
const stat = fs.lstatSync(taskDir);
|
|
230
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
231
|
+
continue;
|
|
232
|
+
const taskMd = path.join(taskDir, 'task.md');
|
|
233
|
+
if (!fs.existsSync(taskMd))
|
|
234
|
+
continue;
|
|
235
|
+
const taskStat = fs.lstatSync(taskMd);
|
|
236
|
+
if (!taskStat.isFile() || taskStat.isSymbolicLink())
|
|
237
|
+
continue;
|
|
238
|
+
const content = fs.readFileSync(taskMd, 'utf8');
|
|
239
|
+
if (!content.includes(`task_create_key_digest: ${keyDigest}`))
|
|
240
|
+
continue;
|
|
241
|
+
if (!content.includes(`task_create_candidate_digest: ${candidateDigest}`)) {
|
|
242
|
+
throw new Error('TASK_CREATE_IDEMPOTENCY_CONFLICT');
|
|
243
|
+
}
|
|
244
|
+
const shortId = loadShortIdByTaskId(repoRoot).get(name)
|
|
245
|
+
?? mutateShortIdRegistry(repoRoot, name, 'alloc').shortId;
|
|
246
|
+
if (!shortId)
|
|
247
|
+
throw new Error('TASK_CREATE_RECEIPT_INVALID');
|
|
248
|
+
return { taskId: name, shortId };
|
|
249
|
+
}
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
function createLocalTask(value, options) {
|
|
253
|
+
const candidate = validateTaskCreateCandidate(value);
|
|
254
|
+
const repoRoot = fs.realpathSync.native(options.repoRoot);
|
|
255
|
+
const workspaceRoot = path.join(repoRoot, '.agents', 'workspace');
|
|
256
|
+
const activeRoot = path.join(workspaceRoot, 'active');
|
|
257
|
+
assertRealDirectory(workspaceRoot, 'TASK_CREATE_WORKSPACE_INVALID');
|
|
258
|
+
assertRealDirectory(activeRoot, 'TASK_CREATE_WORKSPACE_INVALID');
|
|
259
|
+
const project = readProject(repoRoot);
|
|
260
|
+
const canonical = canonicalTaskCreateCandidate(candidate);
|
|
261
|
+
const candidateDigest = sha256(canonical);
|
|
262
|
+
const keyDigest = sha256(candidate.idempotencyKey);
|
|
263
|
+
const receipts = path.join(workspaceRoot, '.task-create');
|
|
264
|
+
return withCreateLock(repoRoot, workspaceRoot, () => {
|
|
265
|
+
if (fs.existsSync(receipts))
|
|
266
|
+
assertRealDirectory(receipts, 'TASK_CREATE_RECEIPT_INVALID');
|
|
267
|
+
else
|
|
268
|
+
fs.mkdirSync(receipts, { mode: 0o700 });
|
|
269
|
+
const receiptPath = path.join(receipts, `${keyDigest}.json`);
|
|
270
|
+
if (fs.existsSync(receiptPath)) {
|
|
271
|
+
const stat = fs.lstatSync(receiptPath);
|
|
272
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
273
|
+
throw new Error('TASK_CREATE_RECEIPT_INVALID');
|
|
274
|
+
const receipt = JSON.parse(fs.readFileSync(receiptPath, 'utf8'));
|
|
275
|
+
if (receipt.candidateDigest !== candidateDigest)
|
|
276
|
+
throw new Error('TASK_CREATE_IDEMPOTENCY_CONFLICT');
|
|
277
|
+
if (!receipt.taskId || !receipt.shortId || !fs.existsSync(path.join(activeRoot, receipt.taskId, 'task.md'))) {
|
|
278
|
+
throw new Error('TASK_CREATE_RECEIPT_INVALID');
|
|
279
|
+
}
|
|
280
|
+
return { status: 'no-op', changed: false, task: { id: receipt.taskId, shortId: receipt.shortId } };
|
|
281
|
+
}
|
|
282
|
+
const recovered = recoverPublishedTask(activeRoot, repoRoot, keyDigest, candidateDigest);
|
|
283
|
+
if (recovered) {
|
|
284
|
+
writeReceipt(receiptPath, {
|
|
285
|
+
version: 1, candidateDigest, taskId: recovered.taskId, shortId: recovered.shortId, status: 'recovered'
|
|
286
|
+
});
|
|
287
|
+
return { status: 'no-op', changed: false, task: { id: recovered.taskId, shortId: recovered.shortId } };
|
|
288
|
+
}
|
|
289
|
+
const base = options.now?.() ?? new Date();
|
|
290
|
+
let taskDate = new Date(base.getTime());
|
|
291
|
+
let taskId = taskIdFor(taskDate);
|
|
292
|
+
while (fs.existsSync(path.join(activeRoot, taskId))) {
|
|
293
|
+
taskDate = new Date(taskDate.getTime() + 1_000);
|
|
294
|
+
taskId = taskIdFor(taskDate);
|
|
295
|
+
}
|
|
296
|
+
const timestamp = formatTimestamp(base);
|
|
297
|
+
const templatePath = path.join(repoRoot, '.agents', 'templates', 'task.md');
|
|
298
|
+
const templateStat = fs.lstatSync(templatePath);
|
|
299
|
+
if (!templateStat.isFile() || templateStat.isSymbolicLink())
|
|
300
|
+
throw new Error('TASK_CREATE_TEMPLATE_INVALID');
|
|
301
|
+
const rendered = renderTask({
|
|
302
|
+
template: fs.readFileSync(templatePath, 'utf8'), candidate, taskId, project, timestamp,
|
|
303
|
+
agentInfraVersion: options.agentInfraVersion ?? 'unknown', keyDigest, candidateDigest
|
|
304
|
+
});
|
|
305
|
+
const temporary = path.join(workspaceRoot, `.task-create.tmp.${process.pid}.${randomUUID()}`);
|
|
306
|
+
fs.mkdirSync(temporary, { mode: 0o700 });
|
|
307
|
+
fs.writeFileSync(path.join(temporary, 'task.md'), rendered, { flag: 'wx', mode: 0o600 });
|
|
308
|
+
const destination = path.join(activeRoot, taskId);
|
|
309
|
+
fs.renameSync(temporary, destination);
|
|
310
|
+
let shortId;
|
|
311
|
+
try {
|
|
312
|
+
const allocated = mutateShortIdRegistry(repoRoot, taskId, 'alloc');
|
|
313
|
+
if (!allocated.shortId)
|
|
314
|
+
throw new Error('TASK_CREATE_SHORT_ID_FAILED');
|
|
315
|
+
shortId = allocated.shortId;
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
fs.rmSync(destination, { recursive: true, force: true });
|
|
319
|
+
throw error;
|
|
320
|
+
}
|
|
321
|
+
try {
|
|
322
|
+
writeReceipt(receiptPath, { version: 1, candidateDigest, taskId, shortId, status: 'created' });
|
|
323
|
+
}
|
|
324
|
+
catch (error) {
|
|
325
|
+
mutateShortIdRegistry(repoRoot, taskId, 'release');
|
|
326
|
+
fs.rmSync(destination, { recursive: true, force: true });
|
|
327
|
+
throw error;
|
|
328
|
+
}
|
|
329
|
+
return { status: 'applied', changed: true, task: { id: taskId, shortId } };
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
export { canonicalTaskCreateCandidate, createLocalTask, validateTaskCreateCandidate };
|
|
333
|
+
//# sourceMappingURL=create.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);
|