@fitlab-ai/agent-infra 0.11.3 → 0.11.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 +17 -8
- package/README.zh-CN.md +17 -8
- package/dist/bin/internal-cli.js +1 -1
- package/dist/lib/internal/cli-route-inventory.js +1 -1
- package/dist/lib/internal/task-artifact.js +1 -1
- package/dist/lib/internal/task-finalization.js +41 -1
- package/dist/lib/internal/task-ledger.js +13 -5
- package/dist/lib/internal/task-operation-registry.js +1 -0
- package/dist/lib/platform/completion-backfill.js +63 -17
- package/dist/lib/sandbox/control/server.js +26 -143
- package/dist/lib/sandbox/task-cleanup.js +13 -20
- package/dist/lib/task/artifact-command.js +11 -4
- package/dist/lib/task/artifact-lifecycle.js +217 -85
- package/dist/lib/task/artifact-operations.js +1 -26
- package/dist/lib/task/artifact-receipts.js +4 -1
- package/dist/lib/task/artifact-schema.js +1 -0
- package/dist/lib/task/artifact-selection.js +25 -0
- package/dist/lib/task/capabilities.js +158 -39
- package/dist/lib/task/commands/status.js +9 -2
- package/dist/lib/task/control-authority.js +3 -2
- package/dist/lib/task/decision-intents.js +12 -0
- package/dist/lib/task/delivery.js +1 -43
- package/dist/lib/task/events.js +80 -64
- package/dist/lib/task/finalization.js +301 -143
- package/dist/lib/task/ledger-intents.js +157 -15
- package/dist/lib/task/lifecycle-path.js +75 -0
- package/dist/lib/task/lifecycle.js +10 -0
- package/dist/lib/task/local-artifact-finalization.js +1 -20
- package/dist/lib/task/orchestration.js +17 -7
- package/dist/lib/task/review-finalization.js +3 -16
- package/dist/lib/task/rework-intent.js +70 -4
- package/dist/lib/task/verification-engine.js +20 -30
- package/dist/lib/task/verification.js +1 -0
- package/dist/lib/task/workflow-warning-intents.js +21 -6
- package/dist/lib/task/workspace-snapshot.js +7 -2
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/internal/cli-route-inventory.ts +1 -1
- package/lib/internal/task-artifact.ts +1 -1
- package/lib/internal/task-finalization.ts +40 -1
- package/lib/internal/task-ledger.ts +12 -5
- package/lib/internal/task-operation-registry.ts +1 -0
- package/lib/platform/completion-backfill.ts +78 -16
- package/lib/sandbox/control/server.ts +21 -146
- package/lib/sandbox/task-cleanup.ts +12 -20
- package/lib/task/artifact-command.ts +14 -4
- package/lib/task/artifact-lifecycle.ts +236 -85
- package/lib/task/artifact-operations.ts +1 -26
- package/lib/task/artifact-receipts.ts +4 -1
- package/lib/task/artifact-schema.ts +1 -0
- package/lib/task/artifact-selection.ts +53 -0
- package/lib/task/capabilities.ts +154 -40
- package/lib/task/commands/status.ts +11 -2
- package/lib/task/control-authority.ts +3 -1
- package/lib/task/decision-intents.ts +9 -0
- package/lib/task/delivery.ts +1 -40
- package/lib/task/events.ts +82 -64
- package/lib/task/finalization.ts +269 -111
- package/lib/task/ledger-intents.ts +155 -15
- package/lib/task/lifecycle-path.ts +98 -0
- package/lib/task/lifecycle.ts +13 -0
- package/lib/task/local-artifact-finalization.ts +2 -25
- package/lib/task/orchestration.ts +26 -8
- package/lib/task/review-finalization.ts +3 -16
- package/lib/task/rework-intent.ts +75 -5
- package/lib/task/verification-engine.ts +19 -30
- package/lib/task/verification.ts +2 -1
- package/lib/task/workflow-warning-intents.ts +18 -6
- package/lib/task/workspace-snapshot.ts +3 -2
- package/package.json +1 -1
- package/templates/.agents/rules/review-handshake.en.md +4 -4
- package/templates/.agents/rules/review-handshake.zh-CN.md +4 -4
- package/templates/.agents/skills/analyze-task/SKILL.en.md +11 -4
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +11 -4
- package/templates/.agents/skills/code-task/SKILL.en.md +14 -8
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +18 -13
- package/templates/.agents/skills/code-task/reference/code-rules.en.md +2 -2
- package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +2 -2
- package/templates/.agents/skills/code-task/reference/dual-mode.en.md +4 -4
- package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +4 -4
- package/templates/.agents/skills/code-task/reference/report-template.en.md +1 -1
- package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +1 -1
- package/templates/.agents/skills/complete-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/plan-task/SKILL.en.md +13 -10
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +13 -10
- package/templates/.agents/skills/review-analysis/SKILL.en.md +5 -1
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +5 -1
- package/templates/.agents/skills/review-code/SKILL.en.md +5 -1
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +5 -1
- package/templates/.agents/skills/review-plan/SKILL.en.md +5 -1
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +5 -1
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +18 -2
- package/templates/.agents/templates/task.en.md +2 -2
- package/templates/.agents/templates/task.zh-CN.md +2 -2
- package/templates/.agents/workflows/bug-fix.en.yaml +11 -6
- package/templates/.agents/workflows/bug-fix.zh-CN.yaml +11 -6
- package/templates/.agents/workflows/feature-development.en.yaml +11 -6
- package/templates/.agents/workflows/feature-development.zh-CN.yaml +11 -6
- package/templates/.agents/workflows/refactoring.en.yaml +11 -6
- package/templates/.agents/workflows/refactoring.zh-CN.yaml +11 -6
- package/dist/lib/sandbox/control/finalization-response.js +0 -33
- package/lib/sandbox/control/finalization-response.ts +0 -35
package/lib/task/capabilities.ts
CHANGED
|
@@ -4,16 +4,19 @@ import path from 'node:path';
|
|
|
4
4
|
import { parseArtifactName } from './artifact-name.ts';
|
|
5
5
|
import type { InvalidationDocument } from './invalidation.ts';
|
|
6
6
|
import { invalidationBlocks, isArtifactInvalidated, parseInvalidationDocument } from './invalidation.ts';
|
|
7
|
-
import { receiptForOutput } from './artifact-receipts.ts';
|
|
7
|
+
import { parseArtifactReceipts, receiptForOutput } from './artifact-receipts.ts';
|
|
8
8
|
import { parseTypedTaskFrontmatter } from './frontmatter.ts';
|
|
9
9
|
import { parseLedgerDocument, summarizeLedgerStage, validateLedgerRows } from './ledger.ts';
|
|
10
10
|
import { parseReviewSummary, resolveCanonicalVerdict } from './review-artifacts.ts';
|
|
11
11
|
import { parseReworkIntentDocument } from './rework-intent.ts';
|
|
12
|
-
import type { ReworkIntent } from './rework-intent.ts';
|
|
12
|
+
import type { ReworkIntent, ReworkTarget } from './rework-intent.ts';
|
|
13
13
|
import { sha256File } from './artifact-receipts.ts';
|
|
14
14
|
import { hasOpenLifecycleExecution } from './activity-log.ts';
|
|
15
15
|
import { parseQualificationAudit, parseTaskQualification } from './qualification-audit.ts';
|
|
16
16
|
import type { QualificationAudit, TaskQualification } from './qualification-audit.ts';
|
|
17
|
+
import { parseLifecyclePathDecision, pathIncludes } from './lifecycle-path.ts';
|
|
18
|
+
import type { LifecyclePathState } from './lifecycle-path.ts';
|
|
19
|
+
import { parseImplementationInputs } from './implementation-inputs.ts';
|
|
17
20
|
|
|
18
21
|
const ARTIFACT_AUDIT_FAMILIES = new Set(['analysis', 'review-analysis', 'plan', 'review-plan', 'code', 'review-code']);
|
|
19
22
|
|
|
@@ -49,6 +52,9 @@ type LifecycleFacts = {
|
|
|
49
52
|
recommendedAction?: LifecycleAction | null;
|
|
50
53
|
qualificationStale?: boolean;
|
|
51
54
|
qualificationStaleArtifacts?: readonly string[];
|
|
55
|
+
pathState?: LifecyclePathState;
|
|
56
|
+
reworkClassificationRequired?: readonly ('analysis' | 'plan' | 'code')[];
|
|
57
|
+
resolvedHumanDecisions?: Partial<Record<'analysis' | 'plan' | 'code', 'review' | 'implementation'>>;
|
|
52
58
|
};
|
|
53
59
|
type CapabilityResult = {
|
|
54
60
|
allowed: boolean;
|
|
@@ -80,6 +86,11 @@ function hasArtifact(facts: LifecycleFacts, action: LifecycleAction): boolean {
|
|
|
80
86
|
return (facts.artifacts[action]?.length ?? 0) > 0;
|
|
81
87
|
}
|
|
82
88
|
|
|
89
|
+
function effectiveReworkTarget(target: ReworkTarget, pathState: LifecyclePathState | undefined): ReworkTarget {
|
|
90
|
+
if (target === 'plan' && pathState?.status === 'valid' && !pathIncludes(pathState, 'plan')) return 'analysis';
|
|
91
|
+
return target;
|
|
92
|
+
}
|
|
93
|
+
|
|
83
94
|
function canStart(action: LifecycleAction, facts: LifecycleFacts, trigger: ExplicitTrigger): CapabilityResult {
|
|
84
95
|
if (!trigger || trigger.requestedAction !== action) return deny('TRIGGER_ACTION_MISMATCH', `requested=${trigger?.requestedAction ?? 'missing'}`);
|
|
85
96
|
if (!trigger.requestId || !trigger.requestId.trim()) return deny('TRIGGER_REQUEST_ID_REQUIRED');
|
|
@@ -93,11 +104,32 @@ function canStart(action: LifecycleAction, facts: LifecycleFacts, trigger: Expli
|
|
|
93
104
|
}
|
|
94
105
|
if (facts.taskState !== 'active') return deny('TASK_NOT_ACTIVE', `state=${facts.taskState}`);
|
|
95
106
|
if (invalidationBlocks(facts.invalidation)) return deny('INVALIDATION_INCOMPLETE');
|
|
96
|
-
if (facts.qualificationStale && facts.recommendedAction !== action) {
|
|
97
|
-
return deny('QUALIFICATION_STALE', ...(facts.qualificationStaleArtifacts ?? ['qualification audit is stale']));
|
|
98
|
-
}
|
|
99
107
|
if (facts.executionBusy) return deny('EXECUTION_BUSY');
|
|
100
108
|
|
|
109
|
+
const pendingIntent = (facts.reworkIntents ?? []).find((intent) => intent.status === 'pending');
|
|
110
|
+
if (pendingIntent) {
|
|
111
|
+
const target = effectiveReworkTarget(pendingIntent.target, facts.pathState);
|
|
112
|
+
const requirementRestart = action === 'analysis' && trigger.reasonCode === 'new-requirement';
|
|
113
|
+
if (!requirementRestart && (target === 'pause' || target !== action)) {
|
|
114
|
+
return deny(target === 'pause' ? 'REWORK_PAUSED' : 'REWORK_INTENT_TARGET_MISMATCH', pendingIntent.intentId);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (facts.pathState && facts.pathState.status !== 'valid') {
|
|
119
|
+
return action === 'analysis'
|
|
120
|
+
? allow(`lifecycle-path-${facts.pathState.status}`)
|
|
121
|
+
: deny('LIFECYCLE_PATH_INVALID', facts.pathState.message);
|
|
122
|
+
}
|
|
123
|
+
if (facts.pathState?.status === 'valid'
|
|
124
|
+
&& ['analysis', 'review-analysis', 'plan', 'review-plan', 'code', 'review-code'].includes(action)
|
|
125
|
+
&& !pathIncludes(facts.pathState, action as never)) {
|
|
126
|
+
return deny('ARTIFACT_STAGE_NOT_SELECTED', `path=${facts.pathState.decision.path}`, `stage=${action}`);
|
|
127
|
+
}
|
|
128
|
+
if (facts.reworkClassificationRequired?.includes(action as 'analysis' | 'plan' | 'code')
|
|
129
|
+
&& !(facts.reworkIntents ?? []).some((intent) => intent.status === 'pending')) {
|
|
130
|
+
return deny('REWORK_CLASSIFICATION_REQUIRED', `stage=${action}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
101
133
|
const implicit = trigger.explicitRequest === false;
|
|
102
134
|
if (action === 'analysis') {
|
|
103
135
|
if (!implicit || hasArtifact(facts, 'analysis') || facts.recommendedAction === 'analysis') return allow('analysis-capability');
|
|
@@ -107,6 +139,9 @@ function canStart(action: LifecycleAction, facts: LifecycleFacts, trigger: Expli
|
|
|
107
139
|
return hasArtifact(facts, 'analysis') ? allow('analysis-artifact') : deny('ANALYSIS_ARTIFACT_REQUIRED');
|
|
108
140
|
}
|
|
109
141
|
if (action === 'plan') {
|
|
142
|
+
if (facts.pathState?.status === 'valid' && facts.pathState.decision.path === 'standard') {
|
|
143
|
+
return hasArtifact(facts, 'analysis') ? allow('analysis-artifact') : deny('ANALYSIS_ARTIFACT_REQUIRED');
|
|
144
|
+
}
|
|
110
145
|
if (!hasArtifact(facts, 'review-analysis')) return deny('ANALYSIS_REVIEW_REQUIRED');
|
|
111
146
|
if (!reviewMatchesLatest(facts, 'analysis', 'review-analysis')) return deny('ANALYSIS_REVIEW_NOT_LATEST');
|
|
112
147
|
if (facts.reviews['review-analysis'] !== 'approved') return deny('ANALYSIS_REVIEW_NOT_APPROVED');
|
|
@@ -118,11 +153,22 @@ function canStart(action: LifecycleAction, facts: LifecycleFacts, trigger: Expli
|
|
|
118
153
|
}
|
|
119
154
|
if (action === 'code') {
|
|
120
155
|
if (trigger.implementationInput) {
|
|
156
|
+
if (facts.resolvedHumanDecisions?.code === 'implementation') return allow('human-decision-implementation-input');
|
|
121
157
|
if (!hasArtifact(facts, 'review-code')) return deny('CODE_REVIEW_REQUIRED');
|
|
122
158
|
if (!reviewMatchesLatest(facts, 'code', 'review-code')) return deny('CODE_REVIEW_NOT_LATEST');
|
|
123
159
|
if (facts.reviews['review-code'] !== 'approved') return deny('CODE_REVIEW_NOT_APPROVED');
|
|
124
160
|
return allow('implementation-input');
|
|
125
161
|
}
|
|
162
|
+
if (facts.pathState?.status === 'valid' && facts.pathState.decision.path === 'streamlined') {
|
|
163
|
+
if (!hasArtifact(facts, 'analysis')) return deny('ANALYSIS_ARTIFACT_REQUIRED');
|
|
164
|
+
if (facts.unresolvedLedger.analysis > 0) return deny('ANALYSIS_LEDGER_BLOCKED', `unresolved=${facts.unresolvedLedger.analysis}`);
|
|
165
|
+
return allow('analysis-path-approved');
|
|
166
|
+
}
|
|
167
|
+
if (facts.pathState?.status === 'valid' && facts.pathState.decision.path === 'standard') {
|
|
168
|
+
if (!hasArtifact(facts, 'plan')) return deny('PLAN_ARTIFACT_REQUIRED');
|
|
169
|
+
if (facts.unresolvedLedger.plan > 0) return deny('PLAN_LEDGER_BLOCKED', `unresolved=${facts.unresolvedLedger.plan}`);
|
|
170
|
+
return allow('plan-path-approved');
|
|
171
|
+
}
|
|
126
172
|
if (!hasArtifact(facts, 'review-plan')) return deny('PLAN_REVIEW_REQUIRED');
|
|
127
173
|
if (!reviewMatchesLatest(facts, 'plan', 'review-plan')) return deny('PLAN_REVIEW_NOT_LATEST');
|
|
128
174
|
if (facts.reviews['review-plan'] !== 'approved') return deny('PLAN_REVIEW_NOT_APPROVED');
|
|
@@ -147,14 +193,6 @@ function latestArtifact(names: readonly string[]): string | null {
|
|
|
147
193
|
.sort((left, right) => right.round - left.round || left.name.localeCompare(right.name))[0]?.name ?? null;
|
|
148
194
|
}
|
|
149
195
|
|
|
150
|
-
function reviewedInputName(content: string, expectedFamily: 'analysis' | 'plan' | 'code'): string | null {
|
|
151
|
-
const header = content.split(/\r?\n/).findIndex((line) => /\*\*(?:审查输入|Review Input)\*\*[::]/.test(line));
|
|
152
|
-
if (header < 0) return null;
|
|
153
|
-
const referenceBlock = content.split(/\r?\n/).slice(header, header + 12).join('\n');
|
|
154
|
-
const expected = new RegExp('`' + expectedFamily + '(?:-r[2-9]|-r[1-9]\\d+)?\\.md`');
|
|
155
|
-
return expected.exec(referenceBlock)?.[0].slice(1, -1) ?? null;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
196
|
function reviewMatchesLatest(facts: LifecycleFacts, source: 'analysis' | 'plan' | 'code', review: 'review-analysis' | 'review-plan' | 'review-code'): boolean {
|
|
159
197
|
const latestSource = latestArtifact(facts.artifacts[source] ?? []);
|
|
160
198
|
return Boolean(latestSource && facts.reviewedInputs?.[review] === latestSource);
|
|
@@ -172,31 +210,53 @@ function qualificationCandidateSnapshotMatches(
|
|
|
172
210
|
});
|
|
173
211
|
}
|
|
174
212
|
|
|
175
|
-
const QUALIFICATION_RECOVERY_ORDER: ReadonlyArray<{ action: LifecycleAction; pattern: RegExp }> = [
|
|
176
|
-
{ action: 'analysis', pattern: /^analysis(?:-r\d+)?\.md$/ },
|
|
177
|
-
{ action: 'review-analysis', pattern: /^review-analysis(?:-r\d+)?\.md$/ },
|
|
178
|
-
{ action: 'plan', pattern: /^plan(?:-r\d+)?\.md$/ },
|
|
179
|
-
{ action: 'review-plan', pattern: /^review-plan(?:-r\d+)?\.md$/ },
|
|
180
|
-
{ action: 'code', pattern: /^code(?:-r\d+)?\.md$/ },
|
|
181
|
-
{ action: 'review-code', pattern: /^review-code(?:-r\d+)?\.md$/ }
|
|
182
|
-
];
|
|
183
|
-
|
|
184
|
-
function qualificationRecoveryAction(facts: LifecycleFacts): LifecycleAction | null {
|
|
185
|
-
const stale = facts.qualificationStaleArtifacts ?? [];
|
|
186
|
-
return QUALIFICATION_RECOVERY_ORDER.find(({ pattern }) => stale.some((name) => pattern.test(name)))?.action ?? null;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
213
|
function recommendNext(facts: LifecycleFacts): LifecycleRecommendation {
|
|
190
|
-
if (facts.
|
|
191
|
-
|
|
192
|
-
return {
|
|
193
|
-
action,
|
|
194
|
-
reasonCode: 'QUALIFICATION_RECOVERY_REQUIRED',
|
|
195
|
-
evidence: facts.qualificationStaleArtifacts ?? ['qualification audit is stale']
|
|
196
|
-
};
|
|
214
|
+
if ((facts.reworkClassificationRequired?.length ?? 0) > 0
|
|
215
|
+
&& !(facts.reworkIntents ?? []).some((intent) => intent.status === 'pending')) {
|
|
216
|
+
return { action: null, reasonCode: 'REWORK_CLASSIFICATION_REQUIRED', evidence: facts.reworkClassificationRequired ?? [] };
|
|
197
217
|
}
|
|
198
218
|
const pendingIntent = (facts.reworkIntents ?? []).find((intent) => intent.status === 'pending');
|
|
199
|
-
if (pendingIntent)
|
|
219
|
+
if (pendingIntent) {
|
|
220
|
+
const target = effectiveReworkTarget(pendingIntent.target, facts.pathState);
|
|
221
|
+
return { action: target === 'pause' ? null : target, reasonCode: target === 'pause' ? 'REWORK_PAUSED' : 'REWORK_INTENT_PENDING', evidence: [pendingIntent.intentId, pendingIntent.findingId] };
|
|
222
|
+
}
|
|
223
|
+
if (facts.pathState && facts.pathState.status !== 'valid') {
|
|
224
|
+
return { action: 'analysis', reasonCode: facts.pathState.status === 'missing' ? 'LIFECYCLE_PATH_MISSING' : 'LIFECYCLE_PATH_INVALID', evidence: [facts.pathState.message] };
|
|
225
|
+
}
|
|
226
|
+
if (facts.pathState?.status === 'valid') {
|
|
227
|
+
for (const stage of facts.pathState.decision.stages) {
|
|
228
|
+
if (stage === 'analysis' && !hasArtifact(facts, stage)) return { action: stage, reasonCode: 'ANALYSIS_ARTIFACT_MISSING', evidence: ['analysis artifact is absent'] };
|
|
229
|
+
if (stage === 'review-analysis' && (!reviewMatchesLatest(facts, 'analysis', stage) || facts.reviews[stage] !== 'approved')) {
|
|
230
|
+
if (facts.reviews[stage] === 'changes-requested' && facts.resolvedHumanDecisions?.analysis === 'review') {
|
|
231
|
+
return { action: stage, reasonCode: 'HUMAN_DECISION_REVIEW_REQUIRED', evidence: ['analysis decision was resolved'] };
|
|
232
|
+
}
|
|
233
|
+
return facts.reviews[stage] === 'changes-requested'
|
|
234
|
+
? { action: 'analysis', reasonCode: 'ANALYSIS_REWORK_REQUIRED', evidence: ['analysis review is not approved'] }
|
|
235
|
+
: { action: stage, reasonCode: 'ANALYSIS_REVIEW_MISSING', evidence: ['analysis review does not bind the latest analysis artifact'] };
|
|
236
|
+
}
|
|
237
|
+
if (stage === 'plan' && !hasArtifact(facts, stage)) return { action: stage, reasonCode: 'PLAN_ARTIFACT_MISSING', evidence: ['plan artifact is absent'] };
|
|
238
|
+
if (stage === 'review-plan' && (!reviewMatchesLatest(facts, 'plan', stage) || facts.reviews[stage] !== 'approved')) {
|
|
239
|
+
if (facts.reviews[stage] === 'changes-requested' && facts.resolvedHumanDecisions?.plan === 'review') {
|
|
240
|
+
return { action: stage, reasonCode: 'HUMAN_DECISION_REVIEW_REQUIRED', evidence: ['plan decision was resolved'] };
|
|
241
|
+
}
|
|
242
|
+
return facts.reviews[stage] === 'changes-requested'
|
|
243
|
+
? { action: 'plan', reasonCode: 'PLAN_REWORK_REQUIRED', evidence: ['plan review is not approved'] }
|
|
244
|
+
: { action: stage, reasonCode: 'PLAN_REVIEW_MISSING', evidence: ['plan review does not bind the latest plan artifact'] };
|
|
245
|
+
}
|
|
246
|
+
if (stage === 'code' && !hasArtifact(facts, stage)) return { action: stage, reasonCode: 'CODE_ARTIFACT_MISSING', evidence: ['code artifact is absent'] };
|
|
247
|
+
if (stage === 'review-code' && (!reviewMatchesLatest(facts, 'code', stage) || facts.reviews[stage] !== 'approved')) {
|
|
248
|
+
if (facts.reviews[stage] === 'changes-requested' && facts.resolvedHumanDecisions?.code) {
|
|
249
|
+
return facts.resolvedHumanDecisions.code === 'implementation'
|
|
250
|
+
? { action: 'code', reasonCode: 'HUMAN_DECISION_IMPLEMENTATION_REQUIRED', evidence: ['code decision requires implementation'] }
|
|
251
|
+
: { action: stage, reasonCode: 'HUMAN_DECISION_REVIEW_REQUIRED', evidence: ['code decision requires no implementation'] };
|
|
252
|
+
}
|
|
253
|
+
return facts.reviews[stage] === 'changes-requested'
|
|
254
|
+
? { action: 'code', reasonCode: 'CODE_REWORK_REQUIRED', evidence: ['code review is not approved'] }
|
|
255
|
+
: { action: stage, reasonCode: 'CODE_REVIEW_MISSING', evidence: ['code review does not bind the latest code artifact'] };
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return { action: null, reasonCode: 'LIFECYCLE_REVIEWED', evidence: ['selected lifecycle path is complete'] };
|
|
259
|
+
}
|
|
200
260
|
if (!hasArtifact(facts, 'analysis')) return { action: 'analysis', reasonCode: 'ANALYSIS_ARTIFACT_MISSING', evidence: ['analysis artifact is absent'] };
|
|
201
261
|
if (!hasArtifact(facts, 'review-analysis') || !reviewMatchesLatest(facts, 'analysis', 'review-analysis')) {
|
|
202
262
|
return { action: 'review-analysis', reasonCode: 'ANALYSIS_REVIEW_MISSING', evidence: ['analysis review does not bind the latest analysis artifact'] };
|
|
@@ -247,8 +307,10 @@ function buildLifecycleFacts(taskDir: string, content: string, taskState = 'acti
|
|
|
247
307
|
const family = familyFor(name);
|
|
248
308
|
return !family || !isArtifactInvalidated(invalidation.document, family, name);
|
|
249
309
|
});
|
|
310
|
+
const allArtifactHashes: Record<string, string> = {};
|
|
311
|
+
for (const name of files) allArtifactHashes[name] = sha256File(path.join(taskDir, name));
|
|
250
312
|
const artifactHashes: Record<string, string> = {};
|
|
251
|
-
for (const name of activeFiles) artifactHashes[name] =
|
|
313
|
+
for (const name of activeFiles) artifactHashes[name] = allArtifactHashes[name]!;
|
|
252
314
|
const artifacts: Partial<Record<LifecycleAction, readonly string[]>> = Object.fromEntries(
|
|
253
315
|
artifactFamilies.map((family) => [
|
|
254
316
|
family, activeFiles.filter((name) => familyFor(name) === family)
|
|
@@ -288,18 +350,67 @@ function buildLifecycleFacts(taskDir: string, content: string, taskState = 'acti
|
|
|
288
350
|
if (!latest) continue;
|
|
289
351
|
const reviewContent = fs.readFileSync(path.join(taskDir, latest), 'utf8');
|
|
290
352
|
const expectedFamily = family === 'review-analysis' ? 'analysis' : family === 'review-plan' ? 'plan' : 'code';
|
|
291
|
-
const
|
|
292
|
-
const
|
|
293
|
-
if (
|
|
353
|
+
const receipt = receiptForOutput(content, latest);
|
|
354
|
+
const input = receipt?.input;
|
|
355
|
+
if (
|
|
356
|
+
input
|
|
357
|
+
&& parseArtifactName(input)?.family === expectedFamily
|
|
358
|
+
&& (artifacts[expectedFamily] ?? []).includes(input)
|
|
359
|
+
&& artifactHashes[input] === receipt.inputSha256
|
|
360
|
+
) reviewedInputs[family] = input;
|
|
294
361
|
const parsed = parseReviewSummary(reviewContent);
|
|
295
362
|
if (!parsed.ok) continue;
|
|
296
363
|
const verdict = resolveCanonicalVerdict(parsed.summary);
|
|
297
364
|
if (verdict.ok) reviews[family] = verdict.verdict === 'Approved' ? 'approved' : verdict.verdict === 'Changes Requested' ? 'changes-requested' : 'rejected';
|
|
298
365
|
}
|
|
366
|
+
const reworkClassificationRequired: Array<'analysis' | 'plan' | 'code'> = [];
|
|
367
|
+
const receipts = parseArtifactReceipts(content).rows;
|
|
368
|
+
for (const [reviewFamily, stage] of [['review-analysis', 'analysis'], ['review-plan', 'plan'], ['review-code', 'code']] as const) {
|
|
369
|
+
const cycles = receipts.filter((receipt) => receipt.event === `${reviewFamily}.completed` && files.includes(receipt.output)
|
|
370
|
+
&& allArtifactHashes[receipt.input] === receipt.inputSha256).map((receipt) => {
|
|
371
|
+
const parsed = parseReviewSummary(fs.readFileSync(path.join(taskDir, receipt.output), 'utf8'));
|
|
372
|
+
const verdict = parsed.ok ? resolveCanonicalVerdict(parsed.summary) : null;
|
|
373
|
+
return {
|
|
374
|
+
input: receipt.input,
|
|
375
|
+
output: receipt.output,
|
|
376
|
+
outputRound: parseArtifactName(receipt.output)?.round ?? 0,
|
|
377
|
+
inputRound: parseArtifactName(receipt.input)?.round ?? 0,
|
|
378
|
+
verdict: verdict?.ok ? verdict.verdict : null
|
|
379
|
+
};
|
|
380
|
+
}).sort((left, right) => left.inputRound - right.inputRound || left.outputRound - right.outputRound || left.output.localeCompare(right.output));
|
|
381
|
+
const latest = cycles.at(-1);
|
|
382
|
+
const trailingInputRounds = new Set<number>();
|
|
383
|
+
for (let index = cycles.length - 1; index >= 0 && cycles[index]!.verdict === 'Changes Requested'; index -= 1) {
|
|
384
|
+
trailingInputRounds.add(cycles[index]!.inputRound);
|
|
385
|
+
}
|
|
386
|
+
const handled = latest && (rework.intents ?? []).some((intent) => intent.sourceArtifact === latest.output
|
|
387
|
+
&& intent.sourceSha256 === allArtifactHashes[latest.output]);
|
|
388
|
+
const latestActiveReview = latestArtifact(artifacts[reviewFamily] ?? []);
|
|
389
|
+
const latestActiveInput = latestArtifact(artifacts[stage] ?? []);
|
|
390
|
+
const latestCycleIsActive = latest?.output === latestActiveReview && latest.input === latestActiveInput;
|
|
391
|
+
if (latestCycleIsActive && latest.verdict === 'Changes Requested' && trailingInputRounds.size >= 2 && !handled) reworkClassificationRequired.push(stage);
|
|
392
|
+
}
|
|
299
393
|
const unresolvedLedger = { analysis: 0, plan: 0, code: 0 };
|
|
300
394
|
if (ledger.present) {
|
|
301
395
|
for (const stage of ['analysis', 'plan', 'code'] as const) unresolvedLedger[stage] = summarizeLedgerStage(ledger.rows, stage).unresolved.length;
|
|
302
396
|
}
|
|
397
|
+
const latestAnalysis = latestArtifact(artifacts.analysis ?? []);
|
|
398
|
+
const pathState = latestAnalysis
|
|
399
|
+
? parseLifecyclePathDecision(fs.readFileSync(path.join(taskDir, latestAnalysis), 'utf8'), latestAnalysis, artifactHashes[latestAnalysis] ?? '')
|
|
400
|
+
: { status: 'missing' as const, decision: null, message: 'analysis artifact is missing' };
|
|
401
|
+
const resolvedHumanDecisions: NonNullable<LifecycleFacts['resolvedHumanDecisions']> = {};
|
|
402
|
+
const implementationInputs = parseImplementationInputs(content).rows;
|
|
403
|
+
for (const stage of ['analysis', 'plan', 'code'] as const) {
|
|
404
|
+
const reviewFamily = `review-${stage}` as 'review-analysis' | 'review-plan' | 'review-code';
|
|
405
|
+
const latestReview = latestArtifact(artifacts[reviewFamily] ?? []);
|
|
406
|
+
if (!latestReview || reviews[reviewFamily] !== 'changes-requested') continue;
|
|
407
|
+
const latestHash = artifactHashes[latestReview];
|
|
408
|
+
const decision = rework.intents.find((intent) => intent.status === 'consumed'
|
|
409
|
+
&& intent.classification === 'human-decision' && intent.sourceArtifact === latestReview && intent.sourceSha256 === latestHash);
|
|
410
|
+
if (!decision) continue;
|
|
411
|
+
const implementationInput = implementationInputs.find((input) => input.ledgerId === decision.findingId);
|
|
412
|
+
resolvedHumanDecisions[stage] = stage === 'code' && implementationInput?.status === 'pending' ? 'implementation' : 'review';
|
|
413
|
+
}
|
|
303
414
|
const facts: LifecycleFacts = {
|
|
304
415
|
taskState, currentStep: String(metadata.current_step ?? ''), artifacts, reviews,
|
|
305
416
|
staleArtifacts, reviewedInputs, artifactHashes,
|
|
@@ -309,6 +420,9 @@ function buildLifecycleFacts(taskDir: string, content: string, taskState = 'acti
|
|
|
309
420
|
recommendedAction: null,
|
|
310
421
|
qualificationStale: qualificationStaleArtifacts.length > 0,
|
|
311
422
|
qualificationStaleArtifacts
|
|
423
|
+
, pathState
|
|
424
|
+
, reworkClassificationRequired
|
|
425
|
+
, resolvedHumanDecisions
|
|
312
426
|
};
|
|
313
427
|
facts.recommendedAction = recommendNext(facts).action;
|
|
314
428
|
return { ok: true, facts };
|
|
@@ -317,5 +431,5 @@ function buildLifecycleFacts(taskDir: string, content: string, taskState = 'acti
|
|
|
317
431
|
}
|
|
318
432
|
}
|
|
319
433
|
|
|
320
|
-
export { buildLifecycleFacts, canStart, recommendNext };
|
|
434
|
+
export { buildLifecycleFacts, canStart, effectiveReworkTarget, recommendNext };
|
|
321
435
|
export type { CapabilityResult, ExplicitTrigger, LifecycleAction, LifecycleFacts, LifecycleFactsResult, LifecycleRecommendation, TriggerInitiator, TriggerReason };
|
|
@@ -386,6 +386,8 @@ type StatusModel = {
|
|
|
386
386
|
orchestration: OrchestrationInfo;
|
|
387
387
|
invalidation?: { status: string; processed: number; total: number; pendingIntents: number };
|
|
388
388
|
recommendation?: string;
|
|
389
|
+
recommendationReason?: string;
|
|
390
|
+
lifecyclePath?: string;
|
|
389
391
|
git: GitInfo;
|
|
390
392
|
};
|
|
391
393
|
|
|
@@ -444,7 +446,11 @@ function renderStatus(model: StatusModel): string[] {
|
|
|
444
446
|
])
|
|
445
447
|
);
|
|
446
448
|
|
|
447
|
-
lines.push('', 'Recommendation', ...renderPairs([
|
|
449
|
+
lines.push('', 'Recommendation', ...renderPairs([
|
|
450
|
+
['path', model.lifecyclePath ?? DASH],
|
|
451
|
+
['action', model.recommendation ?? DASH],
|
|
452
|
+
['reason', model.recommendationReason ?? DASH]
|
|
453
|
+
]));
|
|
448
454
|
|
|
449
455
|
lines.push(
|
|
450
456
|
'',
|
|
@@ -546,7 +552,8 @@ function buildFromResolved(input: BuildStatusModelInput): StatusModel {
|
|
|
546
552
|
pendingIntents
|
|
547
553
|
};
|
|
548
554
|
const facts = buildLifecycleFacts(input.taskDir, content, fm.status ?? 'active');
|
|
549
|
-
const
|
|
555
|
+
const next = facts.ok ? recommendNext(facts.facts) : null;
|
|
556
|
+
const recommendation = next?.action ?? DASH;
|
|
550
557
|
|
|
551
558
|
return {
|
|
552
559
|
taskId: input.taskId,
|
|
@@ -560,6 +567,8 @@ function buildFromResolved(input: BuildStatusModelInput): StatusModel {
|
|
|
560
567
|
orchestration: collectOrchestration(input.taskDir),
|
|
561
568
|
invalidation: invalidationInfo,
|
|
562
569
|
recommendation,
|
|
570
|
+
recommendationReason: next?.reasonCode ?? 'TASK_CAPABILITY_FACTS_INVALID',
|
|
571
|
+
lifecyclePath: facts.ok && facts.facts.pathState?.status === 'valid' ? facts.facts.pathState.decision.path : facts.ok ? facts.facts.pathState?.status ?? DASH : DASH,
|
|
563
572
|
git: collectGit(fm.branch ?? '', run)
|
|
564
573
|
};
|
|
565
574
|
}
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
import type { OrchestrationOptions, OrchestrationResult } from './orchestration.ts';
|
|
27
27
|
import {
|
|
28
28
|
applyTaskFinalization,
|
|
29
|
+
commitPreparedTaskFinalization,
|
|
29
30
|
type TaskFinalizationRequest,
|
|
30
31
|
type TaskFinalizationResult
|
|
31
32
|
} from './finalization.ts';
|
|
@@ -860,7 +861,8 @@ export function dispatchTaskControlOperation(
|
|
|
860
861
|
}
|
|
861
862
|
if (operation.family === 'task-finalization') {
|
|
862
863
|
operationTaskId(context, operation.request.taskRef);
|
|
863
|
-
|
|
864
|
+
const finalize = context.source === 'sandbox-executor' ? commitPreparedTaskFinalization : applyTaskFinalization;
|
|
865
|
+
return finalize(operation.request, {
|
|
864
866
|
repoRoot: context.repoRoot,
|
|
865
867
|
...(context.source === 'sandbox-executor' ? {
|
|
866
868
|
controlBinding: { generation: context.generation, requestId: context.requestId }
|
|
@@ -17,6 +17,7 @@ import { extractSection, extractSubSection, findSectionHeading } from './section
|
|
|
17
17
|
import { captureTaskWriteMetadata, writeTask } from './write.ts';
|
|
18
18
|
import type { TaskMutation, TaskOperationSummary, TaskWriteOptions } from './write.ts';
|
|
19
19
|
import { allowsManualOverride } from './guard-override.ts';
|
|
20
|
+
import { consumeHumanDecisionReworkIntents, parseReworkIntentDocument, reworkIntentMutation } from './rework-intent.ts';
|
|
20
21
|
|
|
21
22
|
type HumanDecisionRequest = {
|
|
22
23
|
taskRef?: string;
|
|
@@ -177,6 +178,14 @@ function applyHumanDecision(request: HumanDecisionRequest, options: TaskWriteOpt
|
|
|
177
178
|
kind: 'section', aliases: ACTIVITY_ALIASES, heading: activity.heading,
|
|
178
179
|
body: appendActivityEntry(activity, { time: metadata.timestamp, step: 'Human Decision', agent: 'human', note: `${row.id} decided → ${recordId}` })
|
|
179
180
|
});
|
|
181
|
+
try {
|
|
182
|
+
const parsedRework = parseReworkIntentDocument(content);
|
|
183
|
+
if (!parsedRework.ok) return failed('DECISION_DOCUMENT_INVALID', parsedRework.message, resolved.taskId, row.id);
|
|
184
|
+
const consumed = consumeHumanDecisionReworkIntents(parsedRework.intents, row.id, metadata.timestamp);
|
|
185
|
+
if (consumed.changed) mutations.push(reworkIntentMutation(content, consumed.intents));
|
|
186
|
+
} catch (error) {
|
|
187
|
+
return failed('DECISION_DOCUMENT_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId, row.id);
|
|
188
|
+
}
|
|
180
189
|
const writeResult = writeTask({ taskRef: resolved.taskId, expectedState: stateOverride ? resolved.state : 'active', mutations, dryRun: request.dryRun }, {
|
|
181
190
|
...options,
|
|
182
191
|
taskLocation: { repoRoot: resolved.repoRoot, taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, state: resolved.state },
|
package/lib/task/delivery.ts
CHANGED
|
@@ -4,8 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
|
|
5
5
|
import { parseTaskFrontmatter } from './frontmatter.ts';
|
|
6
6
|
import { resolveTaskRef } from './resolve-ref.ts';
|
|
7
|
-
import { resolveDeliveryTarget,
|
|
8
|
-
import { extractReviewBaseline, extractReviewDiffBase, extractReviewTargetHead, extractReviewedHead, findAuthoritativeReviewCodeArtifact } from './review-fingerprint.ts';
|
|
7
|
+
import { resolveDeliveryTarget, validateBaseRef, validateRemote } from './delivery-target.ts';
|
|
9
8
|
import { captureTaskWriteMetadata, writeTask } from './write.ts';
|
|
10
9
|
import { withRepositoryMutationLock, withTaskExecutionLock } from './task-execution-lock.ts';
|
|
11
10
|
|
|
@@ -47,40 +46,6 @@ function validateBranch(cwd: string, branch: string): boolean {
|
|
|
47
46
|
return git(cwd, ['check-ref-format', `refs/heads/${branch}`]).status === 0;
|
|
48
47
|
}
|
|
49
48
|
|
|
50
|
-
function validateReviewTargetCompatibility(
|
|
51
|
-
repoRoot: string,
|
|
52
|
-
taskDir: string,
|
|
53
|
-
target: { remote: string; baseRef: string },
|
|
54
|
-
reviewedHead: string
|
|
55
|
-
): { ok: true } | { ok: false; code: string; message: string } {
|
|
56
|
-
const artifact = findAuthoritativeReviewCodeArtifact(taskDir);
|
|
57
|
-
if (!artifact.ok || !artifact.path) return { ok: false, code: 'DELIVERY_REVIEW_REQUIRED', message: 'A current review-code artifact is required before delivery' };
|
|
58
|
-
let content: string;
|
|
59
|
-
try { content = fs.readFileSync(artifact.path, 'utf8'); }
|
|
60
|
-
catch (error) { return { ok: false, code: 'DELIVERY_REVIEW_REQUIRED', message: error instanceof Error ? error.message : String(error) }; }
|
|
61
|
-
const savedReviewedHead = extractReviewedHead(content) || extractReviewBaseline(content);
|
|
62
|
-
const savedTargetHead = extractReviewTargetHead(content);
|
|
63
|
-
const savedDiffBase = extractReviewDiffBase(content);
|
|
64
|
-
if (!savedReviewedHead || !savedTargetHead || !savedDiffBase) {
|
|
65
|
-
return { ok: false, code: 'DELIVERY_REVIEW_TARGET_INVALID', message: 'review-code must save reviewed head, target head, and diff base before delivery' };
|
|
66
|
-
}
|
|
67
|
-
if (savedReviewedHead !== reviewedHead) {
|
|
68
|
-
return { ok: false, code: 'DELIVERY_REVIEW_REQUIRED', message: 'review-code reviewed head does not match last_reviewed_commit' };
|
|
69
|
-
}
|
|
70
|
-
const saved = resolveDiffBase(repoRoot, reviewedHead, savedTargetHead);
|
|
71
|
-
if (!saved.ok || saved.diffBase !== savedDiffBase) {
|
|
72
|
-
return { ok: false, code: 'DELIVERY_REVIEW_TARGET_INVALID', message: saved.ok ? 'saved review diff base does not match saved target head' : saved.message };
|
|
73
|
-
}
|
|
74
|
-
const currentTarget = resolveTargetHead(repoRoot, target);
|
|
75
|
-
if (!currentTarget.ok) return { ok: false, code: currentTarget.code, message: currentTarget.message };
|
|
76
|
-
const current = resolveDiffBase(repoRoot, reviewedHead, currentTarget.head!);
|
|
77
|
-
if (!current.ok) return { ok: false, code: current.code, message: current.message };
|
|
78
|
-
if (current.diffBase !== savedDiffBase) {
|
|
79
|
-
return { ok: false, code: 'DELIVERY_REVIEW_TARGET_CHANGED', message: 'current delivery target changed the reviewed diff base; re-run review-code' };
|
|
80
|
-
}
|
|
81
|
-
return { ok: true };
|
|
82
|
-
}
|
|
83
|
-
|
|
84
49
|
function failure(taskId: string | null, code: string, message: string, retryable = false, status: 'failed' | 'blocked' = retryable ? 'blocked' : 'failed'): TaskBranchDeliveryResult {
|
|
85
50
|
return { status, changed: false, taskId, remoteHead: null, localHead: null, state: null, error: { code, message, retryable } };
|
|
86
51
|
}
|
|
@@ -115,10 +80,6 @@ function deliverUnlocked(
|
|
|
115
80
|
const status = git(repoRoot, ['status', '--porcelain=v1']);
|
|
116
81
|
if (status.status !== 0) return failure(taskId, 'DELIVERY_GIT_INSPECT_FAILED', status.stderr.trim() || 'Unable to inspect Git state');
|
|
117
82
|
if (status.stdout.trim()) return failure(taskId, 'DELIVERY_WORKTREE_DIRTY', 'Working tree must be clean before branch delivery');
|
|
118
|
-
const reviewed = frontmatter.last_reviewed_commit ?? '';
|
|
119
|
-
if (reviewed !== localHead) return failure(taskId, 'DELIVERY_REVIEW_REQUIRED', 'Local HEAD must equal last_reviewed_commit before delivery');
|
|
120
|
-
const reviewTarget = validateReviewTargetCompatibility(repoRoot, taskDir, target.value, reviewed);
|
|
121
|
-
if (!reviewTarget.ok) return failure(taskId, reviewTarget.code, reviewTarget.message, reviewTarget.code.includes('UNAVAILABLE'), reviewTarget.code.includes('UNAVAILABLE') ? 'blocked' : 'failed');
|
|
122
83
|
const ref = `refs/heads/${branch}`;
|
|
123
84
|
const remoteOutput = git(repoRoot, ['ls-remote', '--refs', target.value.remote, ref]);
|
|
124
85
|
if (remoteOutput.status !== 0) return failure(taskId, 'DELIVERY_REMOTE_UNAVAILABLE', remoteOutput.stderr.trim() || 'Unable to inspect the delivery remote', true);
|