@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,171 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { syncPlatformComment } from '../platform/issue-comments.ts';
|
|
5
|
+
import { createPlatformIssue, syncPlatformIssue } from '../platform/issues.ts';
|
|
6
|
+
import type { PlatformResult } from '../platform/types.ts';
|
|
7
|
+
import { VERSION } from '../version.ts';
|
|
8
|
+
import { applyWorkflowWarningIntent } from './workflow-warning-intents.ts';
|
|
9
|
+
import { createLocalTask, type TaskCreateCandidateV1 } from './create.ts';
|
|
10
|
+
import { verifyTaskEvent } from './verification.ts';
|
|
11
|
+
|
|
12
|
+
type TaskCreateStatus = 'applied' | 'no-op' | 'degraded' | 'failed' | 'blocked';
|
|
13
|
+
type TaskCreateOperation = { name: string; status: string; reasonCode: string | null };
|
|
14
|
+
type TaskCreateWarning = { code: string; severity: string; action: string };
|
|
15
|
+
type TaskCreateResult = Readonly<{
|
|
16
|
+
status: TaskCreateStatus;
|
|
17
|
+
changed: boolean;
|
|
18
|
+
task: { id: string | null; shortId: string | null };
|
|
19
|
+
issue: { number: number; url: string } | null;
|
|
20
|
+
operations: readonly TaskCreateOperation[];
|
|
21
|
+
warnings: readonly TaskCreateWarning[];
|
|
22
|
+
error: { code: string; message: string; retryable: boolean } | null;
|
|
23
|
+
}>;
|
|
24
|
+
|
|
25
|
+
type CreateTaskOptions = Readonly<{
|
|
26
|
+
repoRoot: string;
|
|
27
|
+
agentInfraVersion?: string;
|
|
28
|
+
dependencies?: Partial<TaskCreateDependencies>;
|
|
29
|
+
}>;
|
|
30
|
+
|
|
31
|
+
type TaskCreateDependencies = Readonly<{
|
|
32
|
+
createIssue: typeof createPlatformIssue;
|
|
33
|
+
syncIssue: typeof syncPlatformIssue;
|
|
34
|
+
syncComment: typeof syncPlatformComment;
|
|
35
|
+
addWarning: typeof applyWorkflowWarningIntent;
|
|
36
|
+
}>;
|
|
37
|
+
|
|
38
|
+
const DEFAULT_DEPENDENCIES: TaskCreateDependencies = {
|
|
39
|
+
createIssue: createPlatformIssue,
|
|
40
|
+
syncIssue: syncPlatformIssue,
|
|
41
|
+
syncComment: syncPlatformComment,
|
|
42
|
+
addWarning: applyWorkflowWarningIntent
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
function verifyCreatedTask(repoRoot: string, taskId: string): { operation: TaskCreateOperation; error: TaskCreateResult['error'] } {
|
|
46
|
+
const verification = verifyTaskEvent(
|
|
47
|
+
{ taskRef: taskId, event: 'create-task.completed' },
|
|
48
|
+
{ repoRoot }
|
|
49
|
+
);
|
|
50
|
+
if (verification.status === 'pass') {
|
|
51
|
+
return { operation: { name: 'task:verify', status: 'pass', reasonCode: null }, error: null };
|
|
52
|
+
}
|
|
53
|
+
const blocked = verification.status === 'blocked';
|
|
54
|
+
return {
|
|
55
|
+
operation: {
|
|
56
|
+
name: 'task:verify', status: verification.status,
|
|
57
|
+
reasonCode: verification.error?.code ?? 'TASK_CREATE_VERIFICATION_FAILED'
|
|
58
|
+
},
|
|
59
|
+
error: {
|
|
60
|
+
code: verification.error?.code ?? 'TASK_CREATE_VERIFICATION_FAILED',
|
|
61
|
+
message: `Created task did not pass the ${verification.event} gate`,
|
|
62
|
+
retryable: blocked
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function platformOperations(prefix: string, operations: readonly { name: string; status: string; reasonCode?: string | null }[]): TaskCreateOperation[] {
|
|
68
|
+
return operations.map((operation) => ({
|
|
69
|
+
name: `${prefix}:${operation.name}`,
|
|
70
|
+
status: operation.status,
|
|
71
|
+
reasonCode: operation.reasonCode ?? null
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function issueFromTask(repoRoot: string, taskId: string): { number: number; url: string } | null {
|
|
76
|
+
const content = fs.readFileSync(path.join(repoRoot, '.agents', 'workspace', 'active', taskId, 'task.md'), 'utf8');
|
|
77
|
+
const match = /^issue_number:\s*(\d+)\s*$/m.exec(content);
|
|
78
|
+
return match ? { number: Number(match[1]), url: '' } : null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function createTask(value: unknown, options: CreateTaskOptions): TaskCreateResult {
|
|
82
|
+
const dependencies = { ...DEFAULT_DEPENDENCIES, ...options.dependencies };
|
|
83
|
+
let candidate: TaskCreateCandidateV1;
|
|
84
|
+
let local;
|
|
85
|
+
try {
|
|
86
|
+
candidate = value as TaskCreateCandidateV1;
|
|
87
|
+
local = createLocalTask(candidate, {
|
|
88
|
+
repoRoot: options.repoRoot,
|
|
89
|
+
agentInfraVersion: options.agentInfraVersion ?? VERSION
|
|
90
|
+
});
|
|
91
|
+
} catch (error) {
|
|
92
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
93
|
+
const code = /^([A-Z][A-Z0-9_]+)/.exec(message)?.[1] ?? 'TASK_CREATE_FAILED';
|
|
94
|
+
return {
|
|
95
|
+
status: 'failed', changed: false, task: { id: null, shortId: null }, issue: null,
|
|
96
|
+
operations: [], warnings: [], error: { code, message, retryable: code === 'TASK_CREATE_LOCK_TIMEOUT' }
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const operations: TaskCreateOperation[] = [{
|
|
101
|
+
name: 'task:local', status: local.status, reasonCode: null
|
|
102
|
+
}];
|
|
103
|
+
let issue = issueFromTask(options.repoRoot, local.task.id);
|
|
104
|
+
const created = dependencies.createIssue(local.task.id, { cwd: options.repoRoot, agent: candidate.agent });
|
|
105
|
+
operations.push(...platformOperations('platform-create', created.operations));
|
|
106
|
+
if (created.issue) issue = { number: created.issue.number, url: created.issue.url };
|
|
107
|
+
|
|
108
|
+
let platformFailure: PlatformResult | null = created.status === 'failed' || created.status === 'blocked' ? created : null;
|
|
109
|
+
if (!platformFailure && (created.task.issueNumber || issue)) {
|
|
110
|
+
const synced = dependencies.syncIssue(local.task.id, {
|
|
111
|
+
cwd: options.repoRoot,
|
|
112
|
+
agent: candidate.agent,
|
|
113
|
+
status: 'waiting-for-triage',
|
|
114
|
+
assignees: 'current',
|
|
115
|
+
milestone: 'initial',
|
|
116
|
+
issueType: true,
|
|
117
|
+
fields: true
|
|
118
|
+
});
|
|
119
|
+
operations.push(...platformOperations('platform-sync', synced.operations));
|
|
120
|
+
if (synced.issue) issue = { number: synced.issue.number, url: synced.issue.url };
|
|
121
|
+
if (synced.status === 'failed' || synced.status === 'blocked') platformFailure = synced;
|
|
122
|
+
if (!platformFailure) {
|
|
123
|
+
const commented = dependencies.syncComment(local.task.id, { cwd: options.repoRoot, kind: 'task', agent: candidate.agent });
|
|
124
|
+
operations.push(...platformOperations('platform-comment', commented.operations));
|
|
125
|
+
if (commented.status === 'failed' || commented.status === 'blocked') platformFailure = commented;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (platformFailure) {
|
|
130
|
+
const error = platformFailure.error ?? { code: 'ISSUE_CREATE_FAILED', message: 'Platform operation failed', retryable: false };
|
|
131
|
+
dependencies.addWarning({
|
|
132
|
+
kind: 'add', taskRef: local.task.id, step: 'create-task', severity: 'ACTION_REQUIRED',
|
|
133
|
+
code: 'ISSUE_CREATE_FAILED', target: 'issue', message: `${error.code}: ${error.message}`,
|
|
134
|
+
action: 'Fix platform authentication or connectivity, then retry Issue creation for this task.'
|
|
135
|
+
}, { repoRoot: options.repoRoot });
|
|
136
|
+
const verified = verifyCreatedTask(options.repoRoot, local.task.id);
|
|
137
|
+
operations.push(verified.operation);
|
|
138
|
+
if (verified.error) return {
|
|
139
|
+
status: verified.error.retryable ? 'blocked' : 'failed', changed: local.changed,
|
|
140
|
+
task: local.task, issue, operations, warnings: [], error: verified.error
|
|
141
|
+
};
|
|
142
|
+
return {
|
|
143
|
+
status: platformFailure.status === 'blocked' ? 'blocked' : 'degraded',
|
|
144
|
+
changed: local.changed,
|
|
145
|
+
task: local.task,
|
|
146
|
+
issue,
|
|
147
|
+
operations,
|
|
148
|
+
warnings: [{ code: 'ISSUE_CREATE_FAILED', severity: 'ACTION_REQUIRED', action: 'Retry Issue creation after fixing the platform failure.' }],
|
|
149
|
+
error
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const verified = verifyCreatedTask(options.repoRoot, local.task.id);
|
|
154
|
+
operations.push(verified.operation);
|
|
155
|
+
if (verified.error) return {
|
|
156
|
+
status: verified.error.retryable ? 'blocked' : 'failed', changed: local.changed,
|
|
157
|
+
task: local.task, issue, operations, warnings: [], error: verified.error
|
|
158
|
+
};
|
|
159
|
+
return {
|
|
160
|
+
status: local.status,
|
|
161
|
+
changed: local.changed,
|
|
162
|
+
task: local.task,
|
|
163
|
+
issue,
|
|
164
|
+
operations,
|
|
165
|
+
warnings: [],
|
|
166
|
+
error: null
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export { createTask };
|
|
171
|
+
export type { CreateTaskOptions, TaskCreateDependencies, TaskCreateOperation, TaskCreateResult, TaskCreateStatus, TaskCreateWarning };
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { loadShortIdByTaskId, mutateShortIdRegistry } from './short-id.ts';
|
|
6
|
+
import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.ts';
|
|
7
|
+
|
|
8
|
+
const AGENTS = ['claude', 'codex', 'antigravity', 'opencode', 'cursor'] as const;
|
|
9
|
+
const TYPES = ['feature', 'bugfix', 'refactor', 'docs', 'chore'] as const;
|
|
10
|
+
const PRIORITIES = ['Urgent', 'High', 'Medium', 'Low'] as const;
|
|
11
|
+
const EFFORTS = ['High', 'Medium', 'Low'] as const;
|
|
12
|
+
const TASK_INPUT_KEYS = [
|
|
13
|
+
'sources', 'facts', 'constraints', 'decisions', 'alternatives',
|
|
14
|
+
'acceptanceCriteria', 'openQuestions'
|
|
15
|
+
] as const;
|
|
16
|
+
|
|
17
|
+
type TaskAgent = typeof AGENTS[number];
|
|
18
|
+
type TaskType = typeof TYPES[number];
|
|
19
|
+
type TaskPriority = typeof PRIORITIES[number];
|
|
20
|
+
type TaskEffort = typeof EFFORTS[number];
|
|
21
|
+
|
|
22
|
+
type TaskCreateCandidateV1 = Readonly<{
|
|
23
|
+
version: 1;
|
|
24
|
+
idempotencyKey: string;
|
|
25
|
+
agent: TaskAgent;
|
|
26
|
+
title: string;
|
|
27
|
+
type: TaskType;
|
|
28
|
+
branchSlug: string;
|
|
29
|
+
priority: TaskPriority;
|
|
30
|
+
effort: TaskEffort;
|
|
31
|
+
description: string;
|
|
32
|
+
taskInput: Readonly<Record<typeof TASK_INPUT_KEYS[number], readonly string[]>>;
|
|
33
|
+
}>;
|
|
34
|
+
|
|
35
|
+
type LocalTaskCreateResult = Readonly<{
|
|
36
|
+
status: 'applied' | 'no-op';
|
|
37
|
+
changed: boolean;
|
|
38
|
+
task: { id: string; shortId: string };
|
|
39
|
+
}>;
|
|
40
|
+
|
|
41
|
+
type LocalTaskCreateOptions = Readonly<{
|
|
42
|
+
repoRoot: string;
|
|
43
|
+
now?: () => Date;
|
|
44
|
+
agentInfraVersion?: string;
|
|
45
|
+
}>;
|
|
46
|
+
|
|
47
|
+
function payloadError(message: string): never {
|
|
48
|
+
throw new Error(`TASK_CREATE_PAYLOAD_INVALID: ${message}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function exactKeys(value: Record<string, unknown>, expected: readonly string[], label: string): void {
|
|
52
|
+
const actual = Object.keys(value).sort();
|
|
53
|
+
const wanted = [...expected].sort();
|
|
54
|
+
if (actual.length !== wanted.length || actual.some((key, index) => key !== wanted[index])) {
|
|
55
|
+
payloadError(`${label} has unknown or missing fields`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function cleanString(value: unknown, label: string, maxBytes: number, oneLine = false): string {
|
|
60
|
+
if (typeof value !== 'string' || value.length === 0 || Buffer.byteLength(value, 'utf8') > maxBytes) {
|
|
61
|
+
payloadError(`${label} must be a non-empty string within ${maxBytes} bytes`);
|
|
62
|
+
}
|
|
63
|
+
if (/\0|[\u0001-\u0008\u000b\u000c\u000e-\u001f\u007f]/u.test(value) || (oneLine && /[\r\n]/u.test(value))) {
|
|
64
|
+
payloadError(`${label} contains unsupported control characters`);
|
|
65
|
+
}
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function enumValue<T extends string>(value: unknown, allowed: readonly T[], label: string): T {
|
|
70
|
+
if (typeof value !== 'string' || !allowed.includes(value as T)) payloadError(`${label} is invalid`);
|
|
71
|
+
return value as T;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function validateTaskCreateCandidate(value: unknown): TaskCreateCandidateV1 {
|
|
75
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) payloadError('candidate must be an object');
|
|
76
|
+
const candidate = value as Record<string, unknown>;
|
|
77
|
+
exactKeys(candidate, [
|
|
78
|
+
'version', 'idempotencyKey', 'agent', 'title', 'type', 'branchSlug',
|
|
79
|
+
'priority', 'effort', 'description', 'taskInput'
|
|
80
|
+
], 'candidate');
|
|
81
|
+
if (candidate.version !== 1) payloadError('version must be 1');
|
|
82
|
+
const idempotencyKey = cleanString(candidate.idempotencyKey, 'idempotencyKey', 64, true);
|
|
83
|
+
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)) {
|
|
84
|
+
payloadError('idempotencyKey must be a UUID v4');
|
|
85
|
+
}
|
|
86
|
+
const title = cleanString(candidate.title, 'title', 400, true);
|
|
87
|
+
if ([...title].length > 100) payloadError('title exceeds 100 Unicode code points');
|
|
88
|
+
const branchSlug = cleanString(candidate.branchSlug, 'branchSlug', 64, true);
|
|
89
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)+$/.test(branchSlug) || branchSlug.length < 3) {
|
|
90
|
+
payloadError('branchSlug must be lowercase ASCII kebab-case with length 3-64');
|
|
91
|
+
}
|
|
92
|
+
const description = cleanString(candidate.description, 'description', 8 * 1024);
|
|
93
|
+
if (!candidate.taskInput || typeof candidate.taskInput !== 'object' || Array.isArray(candidate.taskInput)) {
|
|
94
|
+
payloadError('taskInput must be an object');
|
|
95
|
+
}
|
|
96
|
+
const rawInput = candidate.taskInput as Record<string, unknown>;
|
|
97
|
+
exactKeys(rawInput, TASK_INPUT_KEYS, 'taskInput');
|
|
98
|
+
const taskInput = Object.fromEntries(TASK_INPUT_KEYS.map((key) => {
|
|
99
|
+
const list = rawInput[key];
|
|
100
|
+
if (!Array.isArray(list) || list.length > 50) payloadError(`taskInput.${key} must contain at most 50 items`);
|
|
101
|
+
return [key, list.map((item, index) => cleanString(item, `taskInput.${key}[${index}]`, 2 * 1024, true))];
|
|
102
|
+
})) as unknown as TaskCreateCandidateV1['taskInput'];
|
|
103
|
+
return {
|
|
104
|
+
version: 1,
|
|
105
|
+
idempotencyKey,
|
|
106
|
+
agent: enumValue(candidate.agent, AGENTS, 'agent'),
|
|
107
|
+
title,
|
|
108
|
+
type: enumValue(candidate.type, TYPES, 'type'),
|
|
109
|
+
branchSlug,
|
|
110
|
+
priority: enumValue(candidate.priority, PRIORITIES, 'priority'),
|
|
111
|
+
effort: enumValue(candidate.effort, EFFORTS, 'effort'),
|
|
112
|
+
description,
|
|
113
|
+
taskInput
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function canonicalTaskCreateCandidate(candidate: TaskCreateCandidateV1): string {
|
|
118
|
+
const valid = validateTaskCreateCandidate(candidate);
|
|
119
|
+
return JSON.stringify({
|
|
120
|
+
version: valid.version,
|
|
121
|
+
idempotencyKey: valid.idempotencyKey,
|
|
122
|
+
agent: valid.agent,
|
|
123
|
+
title: valid.title,
|
|
124
|
+
type: valid.type,
|
|
125
|
+
branchSlug: valid.branchSlug,
|
|
126
|
+
priority: valid.priority,
|
|
127
|
+
effort: valid.effort,
|
|
128
|
+
description: valid.description,
|
|
129
|
+
taskInput: Object.fromEntries(TASK_INPUT_KEYS.map((key) => [key, valid.taskInput[key]]))
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function sha256(value: string): string {
|
|
134
|
+
return createHash('sha256').update(value).digest('hex');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function formatTimestamp(date: Date): string {
|
|
138
|
+
const pad = (value: number) => String(value).padStart(2, '0');
|
|
139
|
+
const offsetMinutes = -date.getTimezoneOffset();
|
|
140
|
+
const sign = offsetMinutes >= 0 ? '+' : '-';
|
|
141
|
+
const offset = Math.abs(offsetMinutes);
|
|
142
|
+
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)}`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function taskIdFor(date: Date): string {
|
|
146
|
+
const pad = (value: number) => String(value).padStart(2, '0');
|
|
147
|
+
return `TASK-${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}-${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function yamlString(value: string): string {
|
|
151
|
+
return JSON.stringify(value);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function markdownBlock(value: string): string {
|
|
155
|
+
return value.split(/\r?\n/).map((line) => /^(?:#{1,6}\s|```)/.test(line) ? `\\${line}` : line).join('\n');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function renderList(items: readonly string[]): string {
|
|
159
|
+
return items.map((item) => `- ${item}`).join('\n');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function replaceEmptySubsection(content: string, heading: string, items: readonly string[]): string {
|
|
163
|
+
if (items.length === 0) return content;
|
|
164
|
+
return content.replace(`${heading}\n`, `${heading}\n\n${renderList(items)}\n`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function renderTask(params: Readonly<{
|
|
168
|
+
template: string;
|
|
169
|
+
candidate: TaskCreateCandidateV1;
|
|
170
|
+
taskId: string;
|
|
171
|
+
project: string;
|
|
172
|
+
timestamp: string;
|
|
173
|
+
agentInfraVersion: string;
|
|
174
|
+
keyDigest: string;
|
|
175
|
+
candidateDigest: string;
|
|
176
|
+
}>): string {
|
|
177
|
+
const { candidate } = params;
|
|
178
|
+
const workflow = candidate.type === 'bugfix' ? 'bug-fix' : candidate.type === 'refactor' ? 'refactoring' : 'feature-development';
|
|
179
|
+
const branch = `${params.project}-${candidate.type}-${candidate.branchSlug}`;
|
|
180
|
+
const frontmatter = [
|
|
181
|
+
'---',
|
|
182
|
+
`id: ${params.taskId}`,
|
|
183
|
+
`type: ${candidate.type}`,
|
|
184
|
+
`branch: ${branch}`,
|
|
185
|
+
`workflow: ${workflow}`,
|
|
186
|
+
'status: active',
|
|
187
|
+
`created_at: ${params.timestamp}`,
|
|
188
|
+
`updated_at: ${params.timestamp}`,
|
|
189
|
+
`agent_infra_version: ${params.agentInfraVersion}`,
|
|
190
|
+
`priority: ${candidate.priority}`,
|
|
191
|
+
`effort: ${candidate.effort}`,
|
|
192
|
+
'start_date:',
|
|
193
|
+
'target_date:',
|
|
194
|
+
'current_step: requirement-analysis',
|
|
195
|
+
`assigned_to: ${candidate.agent}`,
|
|
196
|
+
'pr_status: pending',
|
|
197
|
+
`task_create_key_digest: ${params.keyDigest}`,
|
|
198
|
+
`task_create_candidate_digest: ${params.candidateDigest}`,
|
|
199
|
+
'---'
|
|
200
|
+
].join('\n');
|
|
201
|
+
let content = params.template.replace(/^---\n[\s\S]*?\n---/, frontmatter);
|
|
202
|
+
content = content.replace('# 任务:[标题]', `# 任务:${candidate.title}`);
|
|
203
|
+
content = content.replace('[清晰简洁地描述任务。]', markdownBlock(candidate.description));
|
|
204
|
+
const sections: Array<[string, keyof TaskCreateCandidateV1['taskInput']]> = [
|
|
205
|
+
['### 来源', 'sources'], ['### 已确认事实与证据', 'facts'], ['### 约束', 'constraints'],
|
|
206
|
+
['### 已确认决策', 'decisions'], ['### 候选与否决方案', 'alternatives'],
|
|
207
|
+
['### 验收标准', 'acceptanceCriteria'], ['### 未决事项', 'openQuestions']
|
|
208
|
+
];
|
|
209
|
+
for (const [heading, key] of sections) content = replaceEmptySubsection(content, heading, candidate.taskInput[key]);
|
|
210
|
+
content = content.replace('- **关联 Issue**:#XXX', '- **关联 Issue**:N/A');
|
|
211
|
+
content = content.replace('- **关联 PR**:#XXX', '- **关联 PR**:N/A');
|
|
212
|
+
content = content.replace('- **分支**:`feature/xxx`', `- **分支**:\`${branch}\``);
|
|
213
|
+
const log = [
|
|
214
|
+
`- ${params.timestamp} — **Create Task [started]** by ${candidate.agent} — started`,
|
|
215
|
+
`- ${params.timestamp} — **Create Task** by ${candidate.agent} — Task created from structured candidate`
|
|
216
|
+
].join('\n');
|
|
217
|
+
content = content.replace('\n## 完成检查清单', `\n${log}\n\n## 完成检查清单`);
|
|
218
|
+
return `${content.trimEnd()}\n`;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function readProject(repoRoot: string): string {
|
|
222
|
+
const config = JSON.parse(fs.readFileSync(path.join(repoRoot, '.agents', '.airc.json'), 'utf8')) as { project?: unknown };
|
|
223
|
+
if (typeof config.project !== 'string' || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(config.project)) {
|
|
224
|
+
throw new Error('TASK_CREATE_CONFIG_INVALID: project is missing or invalid');
|
|
225
|
+
}
|
|
226
|
+
return config.project;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function assertRealDirectory(directory: string, code: string): void {
|
|
230
|
+
const stat = fs.lstatSync(directory);
|
|
231
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error(code);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function withCreateLock<T>(repoRoot: string, workspaceRoot: string, operation: () => T): T {
|
|
235
|
+
const lockRoot = path.join(workspaceRoot, '.task-create.lock');
|
|
236
|
+
fs.mkdirSync(lockRoot, { recursive: true, mode: 0o700 });
|
|
237
|
+
assertRealDirectory(lockRoot, 'TASK_CREATE_LOCK_FAILED');
|
|
238
|
+
let callbackError: unknown;
|
|
239
|
+
try {
|
|
240
|
+
return withTaskExecutionLock(repoRoot, 'task-create', 'task-create', () => {
|
|
241
|
+
try { return operation(); }
|
|
242
|
+
catch (error) { callbackError = error; throw error; }
|
|
243
|
+
}, { lockRoot });
|
|
244
|
+
} catch (error) {
|
|
245
|
+
if (callbackError !== undefined) throw callbackError;
|
|
246
|
+
if (error instanceof TaskExecutionLockError && error.code === 'ORCHESTRATION_LOCK_BUSY') {
|
|
247
|
+
throw new Error('TASK_CREATE_LOCK_TIMEOUT');
|
|
248
|
+
}
|
|
249
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
250
|
+
throw new Error(`TASK_CREATE_LOCK_FAILED: ${message}`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function writeReceipt(receiptPath: string, value: unknown): void {
|
|
255
|
+
const temporary = `${receiptPath}.tmp.${process.pid}.${randomUUID()}`;
|
|
256
|
+
fs.writeFileSync(temporary, `${JSON.stringify(value)}\n`, { flag: 'wx', mode: 0o600 });
|
|
257
|
+
try { fs.renameSync(temporary, receiptPath); }
|
|
258
|
+
catch (error) {
|
|
259
|
+
try { fs.unlinkSync(temporary); } catch { /* best effort */ }
|
|
260
|
+
throw error;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function recoverPublishedTask(activeRoot: string, repoRoot: string, keyDigest: string, candidateDigest: string): { taskId: string; shortId: string } | null {
|
|
265
|
+
for (const name of fs.readdirSync(activeRoot)) {
|
|
266
|
+
if (!/^TASK-\d{8}-\d{6}$/.test(name)) continue;
|
|
267
|
+
const taskDir = path.join(activeRoot, name);
|
|
268
|
+
const stat = fs.lstatSync(taskDir);
|
|
269
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) continue;
|
|
270
|
+
const taskMd = path.join(taskDir, 'task.md');
|
|
271
|
+
if (!fs.existsSync(taskMd)) continue;
|
|
272
|
+
const taskStat = fs.lstatSync(taskMd);
|
|
273
|
+
if (!taskStat.isFile() || taskStat.isSymbolicLink()) continue;
|
|
274
|
+
const content = fs.readFileSync(taskMd, 'utf8');
|
|
275
|
+
if (!content.includes(`task_create_key_digest: ${keyDigest}`)) continue;
|
|
276
|
+
if (!content.includes(`task_create_candidate_digest: ${candidateDigest}`)) {
|
|
277
|
+
throw new Error('TASK_CREATE_IDEMPOTENCY_CONFLICT');
|
|
278
|
+
}
|
|
279
|
+
const shortId = loadShortIdByTaskId(repoRoot).get(name)
|
|
280
|
+
?? mutateShortIdRegistry(repoRoot, name, 'alloc').shortId;
|
|
281
|
+
if (!shortId) throw new Error('TASK_CREATE_RECEIPT_INVALID');
|
|
282
|
+
return { taskId: name, shortId };
|
|
283
|
+
}
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function createLocalTask(value: unknown, options: LocalTaskCreateOptions): LocalTaskCreateResult {
|
|
288
|
+
const candidate = validateTaskCreateCandidate(value);
|
|
289
|
+
const repoRoot = fs.realpathSync.native(options.repoRoot);
|
|
290
|
+
const workspaceRoot = path.join(repoRoot, '.agents', 'workspace');
|
|
291
|
+
const activeRoot = path.join(workspaceRoot, 'active');
|
|
292
|
+
assertRealDirectory(workspaceRoot, 'TASK_CREATE_WORKSPACE_INVALID');
|
|
293
|
+
assertRealDirectory(activeRoot, 'TASK_CREATE_WORKSPACE_INVALID');
|
|
294
|
+
const project = readProject(repoRoot);
|
|
295
|
+
const canonical = canonicalTaskCreateCandidate(candidate);
|
|
296
|
+
const candidateDigest = sha256(canonical);
|
|
297
|
+
const keyDigest = sha256(candidate.idempotencyKey);
|
|
298
|
+
const receipts = path.join(workspaceRoot, '.task-create');
|
|
299
|
+
|
|
300
|
+
return withCreateLock(repoRoot, workspaceRoot, () => {
|
|
301
|
+
if (fs.existsSync(receipts)) assertRealDirectory(receipts, 'TASK_CREATE_RECEIPT_INVALID');
|
|
302
|
+
else fs.mkdirSync(receipts, { mode: 0o700 });
|
|
303
|
+
const receiptPath = path.join(receipts, `${keyDigest}.json`);
|
|
304
|
+
if (fs.existsSync(receiptPath)) {
|
|
305
|
+
const stat = fs.lstatSync(receiptPath);
|
|
306
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('TASK_CREATE_RECEIPT_INVALID');
|
|
307
|
+
const receipt = JSON.parse(fs.readFileSync(receiptPath, 'utf8')) as { candidateDigest?: string; taskId?: string; shortId?: string };
|
|
308
|
+
if (receipt.candidateDigest !== candidateDigest) throw new Error('TASK_CREATE_IDEMPOTENCY_CONFLICT');
|
|
309
|
+
if (!receipt.taskId || !receipt.shortId || !fs.existsSync(path.join(activeRoot, receipt.taskId, 'task.md'))) {
|
|
310
|
+
throw new Error('TASK_CREATE_RECEIPT_INVALID');
|
|
311
|
+
}
|
|
312
|
+
return { status: 'no-op', changed: false, task: { id: receipt.taskId, shortId: receipt.shortId } };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const recovered = recoverPublishedTask(activeRoot, repoRoot, keyDigest, candidateDigest);
|
|
316
|
+
if (recovered) {
|
|
317
|
+
writeReceipt(receiptPath, {
|
|
318
|
+
version: 1, candidateDigest, taskId: recovered.taskId, shortId: recovered.shortId, status: 'recovered'
|
|
319
|
+
});
|
|
320
|
+
return { status: 'no-op', changed: false, task: { id: recovered.taskId, shortId: recovered.shortId } };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const base = options.now?.() ?? new Date();
|
|
324
|
+
let taskDate = new Date(base.getTime());
|
|
325
|
+
let taskId = taskIdFor(taskDate);
|
|
326
|
+
while (fs.existsSync(path.join(activeRoot, taskId))) {
|
|
327
|
+
taskDate = new Date(taskDate.getTime() + 1_000);
|
|
328
|
+
taskId = taskIdFor(taskDate);
|
|
329
|
+
}
|
|
330
|
+
const timestamp = formatTimestamp(base);
|
|
331
|
+
const templatePath = path.join(repoRoot, '.agents', 'templates', 'task.md');
|
|
332
|
+
const templateStat = fs.lstatSync(templatePath);
|
|
333
|
+
if (!templateStat.isFile() || templateStat.isSymbolicLink()) throw new Error('TASK_CREATE_TEMPLATE_INVALID');
|
|
334
|
+
const rendered = renderTask({
|
|
335
|
+
template: fs.readFileSync(templatePath, 'utf8'), candidate, taskId, project, timestamp,
|
|
336
|
+
agentInfraVersion: options.agentInfraVersion ?? 'unknown', keyDigest, candidateDigest
|
|
337
|
+
});
|
|
338
|
+
const temporary = path.join(workspaceRoot, `.task-create.tmp.${process.pid}.${randomUUID()}`);
|
|
339
|
+
fs.mkdirSync(temporary, { mode: 0o700 });
|
|
340
|
+
fs.writeFileSync(path.join(temporary, 'task.md'), rendered, { flag: 'wx', mode: 0o600 });
|
|
341
|
+
const destination = path.join(activeRoot, taskId);
|
|
342
|
+
fs.renameSync(temporary, destination);
|
|
343
|
+
let shortId: string;
|
|
344
|
+
try {
|
|
345
|
+
const allocated = mutateShortIdRegistry(repoRoot, taskId, 'alloc');
|
|
346
|
+
if (!allocated.shortId) throw new Error('TASK_CREATE_SHORT_ID_FAILED');
|
|
347
|
+
shortId = allocated.shortId;
|
|
348
|
+
} catch (error) {
|
|
349
|
+
fs.rmSync(destination, { recursive: true, force: true });
|
|
350
|
+
throw error;
|
|
351
|
+
}
|
|
352
|
+
try {
|
|
353
|
+
writeReceipt(receiptPath, { version: 1, candidateDigest, taskId, shortId, status: 'created' });
|
|
354
|
+
} catch (error) {
|
|
355
|
+
mutateShortIdRegistry(repoRoot, taskId, 'release');
|
|
356
|
+
fs.rmSync(destination, { recursive: true, force: true });
|
|
357
|
+
throw error;
|
|
358
|
+
}
|
|
359
|
+
return { status: 'applied', changed: true, task: { id: taskId, shortId } };
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export {
|
|
364
|
+
canonicalTaskCreateCandidate,
|
|
365
|
+
createLocalTask,
|
|
366
|
+
validateTaskCreateCandidate
|
|
367
|
+
};
|
|
368
|
+
export type {
|
|
369
|
+
LocalTaskCreateOptions,
|
|
370
|
+
LocalTaskCreateResult,
|
|
371
|
+
TaskCreateCandidateV1
|
|
372
|
+
};
|
|
@@ -4,9 +4,11 @@ import { appendActivityEntry, locateActivityLog } from './activity-log.ts';
|
|
|
4
4
|
import { isDecisionItem, listDecisionItems, selectDecisionItem } from './decision-items.ts';
|
|
5
5
|
import {
|
|
6
6
|
createImplementationInput,
|
|
7
|
+
finalizeImplementationInput,
|
|
7
8
|
IMPLEMENTATION_INPUT_ALIASES,
|
|
8
9
|
parseImplementationInputs,
|
|
9
|
-
renderImplementationInputs
|
|
10
|
+
renderImplementationInputs,
|
|
11
|
+
selectDeclaredImplementationInput
|
|
10
12
|
} from './implementation-inputs.ts';
|
|
11
13
|
import { LEDGER_COLUMNS, LEDGER_HEADINGS, parseLedger } from './ledger.ts';
|
|
12
14
|
import type { LedgerRow } from './ledger.ts';
|
|
@@ -73,7 +75,7 @@ function existingDecision(
|
|
|
73
75
|
? { recordId, implementationInputId: null }
|
|
74
76
|
: { conflict: '--needs-implementation is only valid for code-stage decisions' };
|
|
75
77
|
const inputs = parseImplementationInputs(content).rows.filter((input) => input.ledgerId === row.id && input.decisionEvidence === row.evidence);
|
|
76
|
-
if (inputs.length !== 1 || inputs[0]!.needsImplementation !== needsImplementation) {
|
|
78
|
+
if (inputs.length !== 1 || (needsImplementation !== undefined && inputs[0]!.needsImplementation !== needsImplementation)) {
|
|
77
79
|
return { conflict: `implementation intent for '${row.id}' conflicts with the existing decision` };
|
|
78
80
|
}
|
|
79
81
|
return { recordId, implementationInputId: inputs[0]!.id };
|
|
@@ -106,9 +108,28 @@ function applyHumanDecision(request: HumanDecisionRequest, options: TaskWriteOpt
|
|
|
106
108
|
return failed('DECISION_TARGET_INVALID', message, resolved.taskId);
|
|
107
109
|
}
|
|
108
110
|
const row = selected.row;
|
|
109
|
-
if (row.stage === 'code' && request.needsImplementation === undefined) return failed('DECISION_PAYLOAD_INVALID', 'code-stage decisions require --needs-implementation true|false', resolved.taskId, row.id);
|
|
110
111
|
if (row.stage !== 'code' && request.needsImplementation !== undefined) return failed('DECISION_PAYLOAD_INVALID', '--needs-implementation is only valid for code-stage decisions', resolved.taskId, row.id);
|
|
111
112
|
|
|
113
|
+
let parsedInputs: ReturnType<typeof parseImplementationInputs> | null = null;
|
|
114
|
+
let declaredInput: ReturnType<typeof selectDeclaredImplementationInput> = null;
|
|
115
|
+
let effectiveNeedsImplementation = request.needsImplementation;
|
|
116
|
+
if (row.stage === 'code') {
|
|
117
|
+
try {
|
|
118
|
+
parsedInputs = parseImplementationInputs(content);
|
|
119
|
+
declaredInput = selectDeclaredImplementationInput(parsedInputs.rows, row.id, row.evidence);
|
|
120
|
+
} catch (error) {
|
|
121
|
+
return failed('DECISION_DOCUMENT_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId, row.id);
|
|
122
|
+
}
|
|
123
|
+
if (declaredInput) {
|
|
124
|
+
if (request.needsImplementation !== undefined && request.needsImplementation !== declaredInput.needsImplementation) {
|
|
125
|
+
return failed('DECISION_CONFLICT', `implementation intent for '${row.id}' conflicts with its declaration`, resolved.taskId, row.id);
|
|
126
|
+
}
|
|
127
|
+
effectiveNeedsImplementation = declaredInput.needsImplementation;
|
|
128
|
+
} else if (effectiveNeedsImplementation === undefined) {
|
|
129
|
+
return failed('DECISION_PAYLOAD_INVALID', 'code-stage decisions require --needs-implementation true|false when no implementation intent was declared', resolved.taskId, row.id);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
112
133
|
let metadata;
|
|
113
134
|
try { metadata = (options.metadataProvider ?? captureTaskWriteMetadata)(); }
|
|
114
135
|
catch (error) { return failed('METADATA_CAPTURE_FAILED', error instanceof Error ? error.message : String(error), resolved.taskId, row.id); }
|
|
@@ -132,18 +153,17 @@ function applyHumanDecision(request: HumanDecisionRequest, options: TaskWriteOpt
|
|
|
132
153
|
];
|
|
133
154
|
let implementationInputId: string | null = null;
|
|
134
155
|
if (row.stage === 'code') {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
implementationInputId = input.id;
|
|
156
|
+
const nextInputs = declaredInput
|
|
157
|
+
? finalizeImplementationInput(parsedInputs!.rows, declaredInput.id, evidence, metadata.timestamp)
|
|
158
|
+
: [...parsedInputs!.rows, createImplementationInput(parsedInputs!.rows, {
|
|
159
|
+
ledgerId: row.id, decisionEvidence: evidence,
|
|
160
|
+
needsImplementation: effectiveNeedsImplementation!, decidedAt: metadata.timestamp
|
|
161
|
+
})];
|
|
162
|
+
implementationInputId = declaredInput?.id ?? nextInputs[nextInputs.length - 1]!.id;
|
|
143
163
|
mutations.push({
|
|
144
164
|
kind: 'section', aliases: IMPLEMENTATION_INPUT_ALIASES,
|
|
145
165
|
heading: findSectionHeading(content, [...IMPLEMENTATION_INPUT_ALIASES]),
|
|
146
|
-
body: renderImplementationInputs(
|
|
166
|
+
body: renderImplementationInputs(nextInputs)
|
|
147
167
|
});
|
|
148
168
|
}
|
|
149
169
|
mutations.push({
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
|
|
3
3
|
import type { AgentClientId } from '../agent-clients/types.ts';
|
|
4
|
+
import { normalizeAgentToken } from '../agent-clients/tokens.ts';
|
|
4
5
|
|
|
5
6
|
type DelegationRole = 'executor' | 'reviewer';
|
|
6
7
|
type DelegationStage = 'analysis' | 'review-analysis' | 'plan' | 'review-plan' | 'code' | 'review-code' | 'commit';
|
|
@@ -145,8 +146,7 @@ function completeDelegationStage(
|
|
|
145
146
|
if (event.stage !== receipt.stage || event.round !== receipt.round || event.artifact !== receipt.artifact) {
|
|
146
147
|
return fail('DELEGATION_STAGE_MISMATCH', 'stage completion identity does not match the active delegation');
|
|
147
148
|
}
|
|
148
|
-
|
|
149
|
-
if (!acceptedAgents.includes(event.agent)) {
|
|
149
|
+
if (normalizeAgentToken(event.agent) !== normalizeAgentToken(receipt.client)) {
|
|
150
150
|
return fail('DELEGATION_AGENT_MISMATCH', `stage agent '${event.agent}' does not match client '${receipt.client}'`);
|
|
151
151
|
}
|
|
152
152
|
return { ok: true, receipt: Object.freeze({ ...receipt, status: 'stage-completed', agent: event.agent }) };
|