@fitlab-ai/agent-infra 0.11.2 → 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 +7 -2
- package/dist/lib/agent-clients/adapter.js +3 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/recovery.js +234 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +8 -35
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +1 -34
- package/dist/lib/agent-clients/adapters/codex.js +10 -0
- package/dist/lib/internal/cli-route-inventory.js +1 -1
- package/dist/lib/internal/manual-validation.js +22 -27
- package/dist/lib/internal/sandbox-control.js +1 -43
- 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-lifecycle.js +1 -1
- package/dist/lib/internal/task-operation-registry.js +7 -2
- package/dist/lib/platform/completion-backfill.js +63 -17
- package/dist/lib/platform/issue-comments.js +2 -31
- package/dist/lib/platform/pr-summary.js +7 -2
- package/dist/lib/sandbox/control/client.js +3 -44
- package/dist/lib/sandbox/control/executor.js +14 -79
- package/dist/lib/sandbox/control/protocol.js +5 -64
- package/dist/lib/sandbox/control/server.js +49 -343
- package/dist/lib/sandbox/control/state.js +1 -7
- package/dist/lib/sandbox/control/task-view.js +1 -2
- 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 +37 -38
- package/dist/lib/task/control-recovery.js +3 -42
- package/dist/lib/task/decision-intents.js +12 -0
- package/dist/lib/task/delegation-receipts.js +2 -2
- package/dist/lib/task/delivery.js +1 -43
- package/dist/lib/task/events.js +80 -64
- package/dist/lib/task/finalization-artifacts.js +1 -3
- package/dist/lib/task/finalization-state.js +1 -1
- package/dist/lib/task/finalization.js +319 -234
- package/dist/lib/task/ledger-intents.js +157 -15
- package/dist/lib/task/lifecycle-path.js +75 -0
- package/dist/lib/task/lifecycle.js +11 -1
- package/dist/lib/task/local-artifact-finalization.js +1 -20
- package/dist/lib/task/manual-validation-receipt.js +8 -3
- package/dist/lib/task/orchestration.js +29 -13
- 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/agent-clients/adapter.ts +46 -0
- package/lib/agent-clients/adapters/codex-lifecycle/recovery.ts +297 -0
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +13 -33
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +1 -44
- package/lib/agent-clients/adapters/codex.ts +10 -0
- package/lib/internal/cli-route-inventory.ts +1 -1
- package/lib/internal/manual-validation.ts +22 -26
- package/lib/internal/sandbox-control.ts +0 -42
- 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-lifecycle.ts +2 -2
- package/lib/internal/task-operation-registry.ts +8 -4
- package/lib/platform/completion-backfill.ts +78 -16
- package/lib/platform/issue-comments.ts +1 -35
- package/lib/platform/pr-summary.ts +10 -2
- package/lib/sandbox/control/client.ts +5 -68
- package/lib/sandbox/control/executor.ts +14 -81
- package/lib/sandbox/control/protocol.ts +7 -83
- package/lib/sandbox/control/server.ts +47 -362
- package/lib/sandbox/control/state.ts +1 -9
- package/lib/sandbox/control/task-view.ts +1 -2
- 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 +41 -48
- package/lib/task/control-recovery.ts +3 -43
- package/lib/task/decision-intents.ts +9 -0
- package/lib/task/delegation-receipts.ts +2 -2
- package/lib/task/delivery.ts +1 -40
- package/lib/task/events.ts +82 -64
- package/lib/task/finalization-artifacts.ts +1 -3
- package/lib/task/finalization-state.ts +1 -1
- package/lib/task/finalization.ts +310 -216
- package/lib/task/ledger-intents.ts +155 -15
- package/lib/task/lifecycle-path.ts +98 -0
- package/lib/task/lifecycle.ts +15 -2
- package/lib/task/local-artifact-finalization.ts +2 -25
- package/lib/task/manual-validation-receipt.ts +8 -2
- package/lib/task/orchestration.ts +56 -27
- 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/lifecycle-orchestration.en.md +19 -5
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +19 -5
- 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-manual-validation/SKILL.en.md +2 -2
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/complete-task/SKILL.en.md +7 -8
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +7 -8
- 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/run-manual-validation/SKILL.en.md +2 -2
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/run-task/SKILL.en.md +9 -8
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +9 -8
- 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 -35
- package/dist/lib/sandbox/control/task-workflow.js +0 -74
- package/dist/lib/sandbox/control/workflow-executor.js +0 -142
- package/dist/lib/task/lifecycle-recovery.js +0 -549
- package/dist/lib/task/recovery-warning.js +0 -19
- package/dist/lib/task/workflow-command.js +0 -21
- package/dist/lib/task/workflow-dispatch.js +0 -213
- package/lib/sandbox/control/finalization-response.ts +0 -37
- package/lib/sandbox/control/task-workflow.ts +0 -95
- package/lib/sandbox/control/workflow-executor.ts +0 -156
- package/lib/task/lifecycle-recovery.ts +0 -691
- package/lib/task/recovery-warning.ts +0 -21
- package/lib/task/workflow-command.ts +0 -22
- package/lib/task/workflow-dispatch.ts +0 -214
|
@@ -10,12 +10,6 @@ import {
|
|
|
10
10
|
renderArtifactSkeleton
|
|
11
11
|
} from './artifact-schema.ts';
|
|
12
12
|
import { withTaskExecutionLock } from './task-execution-lock.ts';
|
|
13
|
-
import {
|
|
14
|
-
buildQualificationAudit,
|
|
15
|
-
expectedQualificationRelations,
|
|
16
|
-
parseTaskQualification,
|
|
17
|
-
renderQualificationAudit
|
|
18
|
-
} from './qualification-audit.ts';
|
|
19
13
|
import type {
|
|
20
14
|
ArtifactSchema,
|
|
21
15
|
ArtifactSchemaFamily,
|
|
@@ -294,23 +288,6 @@ function artifactRound(family: ArtifactSchemaFamily, artifact: string): number |
|
|
|
294
288
|
return identity?.family === family ? identity.round : null;
|
|
295
289
|
}
|
|
296
290
|
|
|
297
|
-
function qualificationAuditForInit(request: ArtifactInitRequest): { ok: true; content: string } | { ok: false; code: string; message: string } {
|
|
298
|
-
let taskContent: string;
|
|
299
|
-
try {
|
|
300
|
-
taskContent = readStableFileSync(path.join(request.taskDir, 'task.md'), { maxBytes: 1024 * 1024 }).bytes.toString('utf8');
|
|
301
|
-
} catch (error) {
|
|
302
|
-
return { ok: false, code: 'ARTIFACT_INIT_TASK_READ_FAILED', message: error instanceof Error ? error.message : String(error) };
|
|
303
|
-
}
|
|
304
|
-
const qualification = parseTaskQualification(taskContent);
|
|
305
|
-
if (!qualification.ok) return qualification;
|
|
306
|
-
if (!qualification.qualification.present) return { ok: true, content: '' };
|
|
307
|
-
const relations = expectedQualificationRelations(taskContent, request.family);
|
|
308
|
-
if (!relations.ok) return relations;
|
|
309
|
-
const audit = buildQualificationAudit(taskContent, { upstreamRelations: relations.relations ?? [] });
|
|
310
|
-
if (!audit.ok) return audit;
|
|
311
|
-
return { ok: true, content: `\n## 资格审计\n\n${renderQualificationAudit(audit.audit)}\n` };
|
|
312
|
-
}
|
|
313
|
-
|
|
314
291
|
function initializeArtifactSkeleton(request: ArtifactInitRequest): ArtifactFileResult {
|
|
315
292
|
const schema = getArtifactSchema(request.family);
|
|
316
293
|
if (!schema) return resultFailure('ARTIFACT_FAMILY_UNKNOWN', `unknown artifact family '${request.family}'`);
|
|
@@ -333,9 +310,7 @@ function initializeArtifactSkeleton(request: ArtifactInitRequest): ArtifactFileR
|
|
|
333
310
|
artifact: request.artifact,
|
|
334
311
|
...(request.locale ? { locale: request.locale } : {})
|
|
335
312
|
});
|
|
336
|
-
const
|
|
337
|
-
if (!audit.ok) return resultFailure(audit.code, audit.message);
|
|
338
|
-
const content = `${skeleton.trimEnd()}${audit.content}`;
|
|
313
|
+
const content = skeleton.trimEnd();
|
|
339
314
|
const tempPath = path.join(request.taskDir, `.${request.artifact}.init-${process.pid}-${Date.now()}.tmp`);
|
|
340
315
|
try {
|
|
341
316
|
fs.writeFileSync(tempPath, content, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
@@ -64,7 +64,10 @@ function validateReceiptShape(event: ArtifactReceiptEvent, output: string, input
|
|
|
64
64
|
const inputIdentity = parseArtifactName(input);
|
|
65
65
|
if (!outputIdentity || !inputIdentity) throw new ArtifactReceiptError(`receipt artifact identity is invalid: ${output} -> ${input}`);
|
|
66
66
|
const shape = RECEIPT_SHAPES[event];
|
|
67
|
-
|
|
67
|
+
const inputMatches = event === 'code.completed'
|
|
68
|
+
? inputIdentity.family === 'analysis' || inputIdentity.family === 'plan'
|
|
69
|
+
: inputIdentity.family === shape.input;
|
|
70
|
+
if (outputIdentity.family !== shape.output || !inputMatches) {
|
|
68
71
|
throw new ArtifactReceiptError(`receipt event '${event}' does not match ${output} -> ${input}`);
|
|
69
72
|
}
|
|
70
73
|
}
|
|
@@ -30,6 +30,7 @@ const LOCAL_SECTIONS = {
|
|
|
30
30
|
['impact', '影响评估', 'Impact Assessment'],
|
|
31
31
|
['risks', '技术风险', 'Technical Risks'],
|
|
32
32
|
['effort', '工作量和复杂度评估', 'Effort and Complexity Assessment'],
|
|
33
|
+
['flow-decision', '流程裁定', 'Flow Decision'],
|
|
33
34
|
['state-check', '状态核对', 'State Check']
|
|
34
35
|
],
|
|
35
36
|
plan: [
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { ArtifactFamily } from './artifact-name.ts';
|
|
2
|
+
|
|
3
|
+
type SelectionArtifact = Readonly<{
|
|
4
|
+
family: ArtifactFamily;
|
|
5
|
+
round: number;
|
|
6
|
+
name: string;
|
|
7
|
+
}>;
|
|
8
|
+
|
|
9
|
+
type ArtifactDisposition = 'create' | 'resume' | 'reuse';
|
|
10
|
+
|
|
11
|
+
type ArtifactSelection = Readonly<{
|
|
12
|
+
disposition: ArtifactDisposition;
|
|
13
|
+
reasonCode: string;
|
|
14
|
+
artifact: SelectionArtifact;
|
|
15
|
+
writeRequired: boolean;
|
|
16
|
+
}>;
|
|
17
|
+
|
|
18
|
+
type ArtifactSelectionRequest = Readonly<{
|
|
19
|
+
next: SelectionArtifact;
|
|
20
|
+
latest: SelectionArtifact | null;
|
|
21
|
+
open: boolean;
|
|
22
|
+
hasChangeEvidence: boolean;
|
|
23
|
+
}>;
|
|
24
|
+
|
|
25
|
+
function selectArtifactDisposition(request: ArtifactSelectionRequest): ArtifactSelection {
|
|
26
|
+
if (request.open) {
|
|
27
|
+
return {
|
|
28
|
+
disposition: 'resume', reasonCode: 'open-round', artifact: request.next,
|
|
29
|
+
writeRequired: true
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
if (!request.latest) {
|
|
33
|
+
return {
|
|
34
|
+
disposition: 'create',
|
|
35
|
+
reasonCode: request.next.round === 1 ? 'no-history' : 'invalidated-history',
|
|
36
|
+
artifact: request.next, writeRequired: true
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const reasonCode = request.hasChangeEvidence ? 'change-evidence' : 'history-matched';
|
|
40
|
+
const reuse = reasonCode === 'history-matched';
|
|
41
|
+
return {
|
|
42
|
+
disposition: reuse ? 'reuse' : 'create',
|
|
43
|
+
reasonCode,
|
|
44
|
+
artifact: reuse ? request.latest : request.next,
|
|
45
|
+
writeRequired: !reuse
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export { selectArtifactDisposition };
|
|
50
|
+
export type {
|
|
51
|
+
ArtifactDisposition,
|
|
52
|
+
ArtifactSelection,
|
|
53
|
+
};
|
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
|
}
|