codex-workflow-v2 2.0.0-alpha.3 → 2.0.0-alpha.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 +23 -2
- package/dist/src/cli.js +24 -6
- package/dist/src/cli.js.map +1 -1
- package/dist/src/contracts.d.ts +60 -1
- package/dist/src/delegation.d.ts +8 -0
- package/dist/src/delegation.js +155 -0
- package/dist/src/delegation.js.map +1 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/state/store.d.ts +5 -1
- package/dist/src/state/store.js +13 -0
- package/dist/src/state/store.js.map +1 -1
- package/dist/src/ulid.d.ts +1 -1
- package/dist/src/ulid.js.map +1 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/dist/src/workflow.d.ts +14 -6
- package/dist/src/workflow.js +154 -9
- package/dist/src/workflow.js.map +1 -1
- package/docs/decisions.md +7 -0
- package/docs/delegated-approval.md +164 -0
- package/docs/development-flow.md +13 -1
- package/docs/pdf/codex-workflow-v2-architecture-ru.pdf +334 -337
- package/docs/pdf/codex-workflow-v2-chat-only-guide-ru.pdf +763 -612
- package/docs/pdf/codex-workflow-v2-technical-reference-ru.pdf +502 -447
- package/docs/updating-existing-project.md +89 -0
- package/docs/validation-report.md +17 -7
- package/package.json +1 -1
- package/plugins/codex-workflow-gateway/references/protocol.md +13 -0
- package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +22 -2
- package/references/state-machine.md +12 -0
- package/schemas/authorization-event.schema.json +55 -0
- package/schemas/delegation-grant.schema.json +69 -0
package/dist/src/version.js
CHANGED
package/dist/src/workflow.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ContextEnvelope, type DiscoveryState, type MilestonePlanInput, type MilestoneState, type KnowledgeSelection, type GraphBinding, type GraphKind, type GraphMode, type GraphRefreshEvidence, type GraphRefreshRequest, type ProjectKnowledgeMap, type ReviewInput, type TaskPlanInput, type TaskState, type WriterLease, type WorkflowRole, type WorkspaceOwner } from './contracts.js';
|
|
1
|
+
import { type ContextEnvelope, type DelegationGrant, type DelegationGrantGate, type DelegationPolicyInput, type DiscoveryState, type MilestonePlanInput, type MilestoneState, type KnowledgeSelection, type GraphBinding, type GraphKind, type GraphMode, type GraphRefreshEvidence, type GraphRefreshRequest, type ProjectKnowledgeMap, type ReviewInput, type TaskPlanInput, type TaskState, type WriterLease, type WorkflowRole, type WorkspaceOwner } from './contracts.js';
|
|
2
2
|
import { type RepositoryIdentity } from './repository.js';
|
|
3
3
|
import { WriterLockManager } from './state/lock.js';
|
|
4
4
|
import { FileStateStore } from './state/store.js';
|
|
@@ -29,6 +29,9 @@ export declare class WorkflowService {
|
|
|
29
29
|
private readonly now;
|
|
30
30
|
constructor(store?: FileStateStore, now?: () => Date);
|
|
31
31
|
context(repository: string): WorkflowContext;
|
|
32
|
+
prepareDelegation(repository: string, input: DelegationPolicyInput): DelegationGrantGate;
|
|
33
|
+
grantDelegation(repository: string, input: DelegationPolicyInput, confirmationCode: string): DelegationGrant;
|
|
34
|
+
revokeDelegation(repository: string, grantId: string, expectedRevision: number, actor: string, reason: string): DelegationGrant;
|
|
32
35
|
startDiscovery(repository: string, input: DiscoveryInput): DiscoveryState;
|
|
33
36
|
updateDiscovery(repository: string, discoveryId: string, expectedRevision: number, patch: DiscoveryPatch): DiscoveryState;
|
|
34
37
|
materializeTask(repository: string, discoveryId: string, expectedRevision: number, title: string, baseBranch?: string, milestoneId?: string | null): {
|
|
@@ -42,13 +45,13 @@ export declare class WorkflowService {
|
|
|
42
45
|
milestoneRoot: string;
|
|
43
46
|
};
|
|
44
47
|
setMilestonePlan(repository: string, milestoneId: string, expectedRevision: number, plan: MilestonePlanInput): MilestoneState;
|
|
45
|
-
authorizeMilestone(repository: string, milestoneId: string, expectedRevision: number, actor: string, reason?: string): MilestoneState;
|
|
48
|
+
authorizeMilestone(repository: string, milestoneId: string, expectedRevision: number, actor: string, reason?: string, delegationGrantId?: string | null): MilestoneState;
|
|
46
49
|
validateMilestone(repository: string, milestoneId: string, expectedRevision: number): MilestoneState;
|
|
47
|
-
acceptMilestone(repository: string, milestoneId: string, expectedRevision: number, actor: string, confirmationCode: string): MilestoneState;
|
|
50
|
+
acceptMilestone(repository: string, milestoneId: string, expectedRevision: number, actor: string, confirmationCode: string, delegationGrantId?: string | null): MilestoneState;
|
|
48
51
|
cancelMilestone(repository: string, milestoneId: string, expectedRevision: number, reason: string): MilestoneState;
|
|
49
52
|
setTaskPlan(repository: string, taskId: string, expectedRevision: number, plan: TaskPlanInput): TaskState;
|
|
50
53
|
rebindTaskKnowledge(repository: string, taskId: string, expectedRevision: number): TaskState;
|
|
51
|
-
authorizeTask(repository: string, taskId: string, expectedRevision: number, actor: string, reason?: string): TaskState;
|
|
54
|
+
authorizeTask(repository: string, taskId: string, expectedRevision: number, actor: string, reason?: string, delegationGrantId?: string | null): TaskState;
|
|
52
55
|
startTask(repository: string, taskId: string, expectedRevision: number, workspaceOwner: WorkspaceOwner): TaskState;
|
|
53
56
|
runStep(repository: string, taskId: string, stepId: string, expectedRevision: number, owner: string, writerToken: string | null): {
|
|
54
57
|
task: TaskState;
|
|
@@ -60,7 +63,7 @@ export declare class WorkflowService {
|
|
|
60
63
|
submitTask(repository: string, taskId: string, expectedRevision: number, writerToken: string): TaskState;
|
|
61
64
|
recordReview(repository: string, taskId: string, expectedRevision: number, review: ReviewInput): TaskState;
|
|
62
65
|
setTaskResult(repository: string, taskId: string, expectedRevision: number, summary: string, limitations: string[]): TaskState;
|
|
63
|
-
acceptTask(repository: string, taskId: string, expectedRevision: number, actor: string): TaskState;
|
|
66
|
+
acceptTask(repository: string, taskId: string, expectedRevision: number, actor: string, delegationGrantId?: string | null): TaskState;
|
|
64
67
|
syncTaskBase(repository: string, taskId: string, expectedRevision: number): TaskState;
|
|
65
68
|
mergeTask(repository: string, taskId: string, expectedRevision: number, writerToken: string): TaskState;
|
|
66
69
|
confirmExternalMerge(repository: string, taskId: string, expectedRevision: number, writerToken: string): TaskState;
|
|
@@ -69,11 +72,12 @@ export declare class WorkflowService {
|
|
|
69
72
|
discoveries: DiscoveryState[];
|
|
70
73
|
tasks: TaskState[];
|
|
71
74
|
milestones: MilestoneState[];
|
|
75
|
+
delegations: DelegationGrant[];
|
|
72
76
|
};
|
|
73
77
|
scanKnowledgeMap(repository: string): ProjectKnowledgeMap;
|
|
74
78
|
showKnowledgeMap(repository: string): ProjectKnowledgeMap;
|
|
75
79
|
knowledgeMapStatus(repository: string): ProjectKnowledgeMap;
|
|
76
|
-
approveKnowledgeMap(repository: string, expectedRevision: number, selections: KnowledgeSelection[], actor: string): ProjectKnowledgeMap;
|
|
80
|
+
approveKnowledgeMap(repository: string, expectedRevision: number, selections: KnowledgeSelection[], actor: string, delegationGrantId?: string | null): ProjectKnowledgeMap;
|
|
77
81
|
reconcileKnowledgeMap(repository: string, expectedRevision: number): ProjectKnowledgeMap;
|
|
78
82
|
graphRefreshRequest(repository: string, graphKind: GraphKind, mode?: GraphMode): GraphRefreshRequest;
|
|
79
83
|
graphStatus(repository: string, graphKind: GraphKind, mode?: GraphMode): {
|
|
@@ -83,6 +87,10 @@ export declare class WorkflowService {
|
|
|
83
87
|
bindGraph(repository: string, expectedRevision: number | null, request: GraphRefreshRequest, evidence: GraphRefreshEvidence): GraphBinding;
|
|
84
88
|
recordGraphFallback(repository: string, graphKind: GraphKind, mode: GraphMode, expectedRevision: number | null, reason: string): GraphBinding;
|
|
85
89
|
next(repository: string): Record<string, unknown>;
|
|
90
|
+
private delegatedAuthorization;
|
|
91
|
+
private nextForTaskWithDelegation;
|
|
92
|
+
private delegatedApprovalOptions;
|
|
93
|
+
private assertDelegationScopeExists;
|
|
86
94
|
private assertExecutionAuthorizationFresh;
|
|
87
95
|
private assertMilestoneAuthorizationFresh;
|
|
88
96
|
private readGraphBindingOrNull;
|
package/dist/src/workflow.js
CHANGED
|
@@ -3,6 +3,7 @@ import { createHash } from 'node:crypto';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { PROTOCOL_VERSION, STATE_SCHEMA_VERSION, } from './contracts.js';
|
|
5
5
|
import { renderBrief, renderPlan, renderResult } from './artifacts.js';
|
|
6
|
+
import { assertDelegationCanAuthorize, delegationCanAuthorize, prepareDelegationGrantGate, } from './delegation.js';
|
|
6
7
|
import { assessDiscovery, mergeUnique } from './domain/discovery.js';
|
|
7
8
|
import { pathAllowed, validatePlan } from './domain/validation.js';
|
|
8
9
|
import { WorkflowError } from './errors.js';
|
|
@@ -30,6 +31,70 @@ export class WorkflowService {
|
|
|
30
31
|
locks: new WriterLockManager(this.store.lockRoot(identity.projectId), this.now),
|
|
31
32
|
};
|
|
32
33
|
}
|
|
34
|
+
prepareDelegation(repository, input) {
|
|
35
|
+
const { identity } = this.context(repository);
|
|
36
|
+
const gate = prepareDelegationGrantGate(identity.projectId, input, this.now());
|
|
37
|
+
this.assertDelegationScopeExists(identity.projectId, gate.policy);
|
|
38
|
+
return gate;
|
|
39
|
+
}
|
|
40
|
+
grantDelegation(repository, input, confirmationCode) {
|
|
41
|
+
const { identity } = this.context(repository);
|
|
42
|
+
const gate = prepareDelegationGrantGate(identity.projectId, input, this.now());
|
|
43
|
+
this.assertDelegationScopeExists(identity.projectId, gate.policy);
|
|
44
|
+
if (confirmationCode.trim() !== gate.confirmationCode) {
|
|
45
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Delegation confirmation does not match the exact project and policy.', { projectId: identity.projectId, policyHash: gate.policyHash });
|
|
46
|
+
}
|
|
47
|
+
const duplicate = this.store.listDelegations(identity.projectId)
|
|
48
|
+
.find((grant) => grant.policyHash === gate.policyHash);
|
|
49
|
+
if (duplicate) {
|
|
50
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'This exact delegation policy was already issued.', {
|
|
51
|
+
grantId: duplicate.id,
|
|
52
|
+
status: duplicate.status,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
const timestamp = this.now().toISOString();
|
|
56
|
+
return this.store.writeDelegation({
|
|
57
|
+
schemaVersion: STATE_SCHEMA_VERSION,
|
|
58
|
+
revision: 0,
|
|
59
|
+
id: createEntityId('DGR', this.now().getTime()),
|
|
60
|
+
projectId: identity.projectId,
|
|
61
|
+
createdAt: timestamp,
|
|
62
|
+
updatedAt: timestamp,
|
|
63
|
+
kind: 'delegation-grant',
|
|
64
|
+
status: 'active',
|
|
65
|
+
principal: gate.policy.principal,
|
|
66
|
+
delegate: gate.policy.delegate,
|
|
67
|
+
scope: gate.policy.scope,
|
|
68
|
+
transitions: gate.policy.transitions,
|
|
69
|
+
expiresAt: gate.policy.expiresAt,
|
|
70
|
+
policyHash: gate.policyHash,
|
|
71
|
+
revokedAt: null,
|
|
72
|
+
revokedBy: null,
|
|
73
|
+
revokeReason: null,
|
|
74
|
+
}, null);
|
|
75
|
+
}
|
|
76
|
+
revokeDelegation(repository, grantId, expectedRevision, actor, reason) {
|
|
77
|
+
const { identity } = this.context(repository);
|
|
78
|
+
const grant = this.store.readDelegation(identity.projectId, grantId);
|
|
79
|
+
assertExpectedRevision(grant, expectedRevision);
|
|
80
|
+
if (grant.status !== 'active') {
|
|
81
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Delegation ${grantId} is already ${grant.status}.`);
|
|
82
|
+
}
|
|
83
|
+
if (actor.trim() !== grant.principal) {
|
|
84
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Only the recorded principal may revoke a delegation.', {
|
|
85
|
+
principal: grant.principal,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
if (!reason.trim())
|
|
89
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Delegation revocation requires a reason.');
|
|
90
|
+
return this.store.writeDelegation({
|
|
91
|
+
...grant,
|
|
92
|
+
status: 'revoked',
|
|
93
|
+
revokedAt: this.now().toISOString(),
|
|
94
|
+
revokedBy: grant.principal,
|
|
95
|
+
revokeReason: reason.trim(),
|
|
96
|
+
}, expectedRevision);
|
|
97
|
+
}
|
|
33
98
|
startDiscovery(repository, input) {
|
|
34
99
|
if (!input.goal.trim())
|
|
35
100
|
throw new WorkflowError('INVALID_ARGUMENT', 'Discovery requires a substantive goal.');
|
|
@@ -266,7 +331,7 @@ export class WorkflowService {
|
|
|
266
331
|
this.store.publishArtifact(stagedPlan);
|
|
267
332
|
return saved;
|
|
268
333
|
}
|
|
269
|
-
authorizeMilestone(repository, milestoneId, expectedRevision, actor, reason = '') {
|
|
334
|
+
authorizeMilestone(repository, milestoneId, expectedRevision, actor, reason = '', delegationGrantId = null) {
|
|
270
335
|
const { identity } = this.context(repository);
|
|
271
336
|
const milestone = this.store.readMilestone(identity.projectId, milestoneId);
|
|
272
337
|
assertExpectedRevision(milestone, expectedRevision);
|
|
@@ -278,6 +343,7 @@ export class WorkflowService {
|
|
|
278
343
|
if (planHash !== milestone.planHash) {
|
|
279
344
|
throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone Plan changed after it was recorded.');
|
|
280
345
|
}
|
|
346
|
+
const delegation = this.delegatedAuthorization(identity.projectId, delegationGrantId, 'milestone.execution_authorize', actor, milestone);
|
|
281
347
|
const event = {
|
|
282
348
|
kind: 'execution',
|
|
283
349
|
decision: 'approved',
|
|
@@ -289,6 +355,8 @@ export class WorkflowService {
|
|
|
289
355
|
evidenceHash: null,
|
|
290
356
|
headCommit: runGit(identity.repositoryRoot, ['rev-parse', milestone.baseBranch]),
|
|
291
357
|
reason,
|
|
358
|
+
authorizationMode: delegation ? 'delegated' : 'human',
|
|
359
|
+
...(delegation ? { delegation } : {}),
|
|
292
360
|
};
|
|
293
361
|
return this.store.writeMilestone({ ...milestone, status: 'active', planHash, authorizations: [...milestone.authorizations, event] }, expectedRevision);
|
|
294
362
|
}
|
|
@@ -355,7 +423,7 @@ export class WorkflowService {
|
|
|
355
423
|
this.store.publishArtifact(stagedResult);
|
|
356
424
|
return saved;
|
|
357
425
|
}
|
|
358
|
-
acceptMilestone(repository, milestoneId, expectedRevision, actor, confirmationCode) {
|
|
426
|
+
acceptMilestone(repository, milestoneId, expectedRevision, actor, confirmationCode, delegationGrantId = null) {
|
|
359
427
|
const { identity } = this.context(repository);
|
|
360
428
|
const milestone = this.store.readMilestone(identity.projectId, milestoneId);
|
|
361
429
|
assertExpectedRevision(milestone, expectedRevision);
|
|
@@ -384,6 +452,7 @@ export class WorkflowService {
|
|
|
384
452
|
revision: milestone.revision,
|
|
385
453
|
});
|
|
386
454
|
}
|
|
455
|
+
const delegation = this.delegatedAuthorization(identity.projectId, delegationGrantId, 'milestone.final_accept', normalizedActor, milestone);
|
|
387
456
|
const event = {
|
|
388
457
|
kind: 'final_acceptance',
|
|
389
458
|
decision: 'approved',
|
|
@@ -395,6 +464,8 @@ export class WorkflowService {
|
|
|
395
464
|
evidenceHash,
|
|
396
465
|
headCommit: currentHead,
|
|
397
466
|
reason: `Confirmed with ${gate.confirmationCode}.`,
|
|
467
|
+
authorizationMode: delegation ? 'delegated' : 'human',
|
|
468
|
+
...(delegation ? { delegation } : {}),
|
|
398
469
|
};
|
|
399
470
|
return this.store.writeMilestone({
|
|
400
471
|
...milestone,
|
|
@@ -559,7 +630,7 @@ export class WorkflowService {
|
|
|
559
630
|
this.store.publishArtifact(stagedPlan);
|
|
560
631
|
return saved;
|
|
561
632
|
}
|
|
562
|
-
authorizeTask(repository, taskId, expectedRevision, actor, reason = '') {
|
|
633
|
+
authorizeTask(repository, taskId, expectedRevision, actor, reason = '', delegationGrantId = null) {
|
|
563
634
|
const { identity } = this.context(repository);
|
|
564
635
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
565
636
|
assertExpectedRevision(task, expectedRevision);
|
|
@@ -570,6 +641,7 @@ export class WorkflowService {
|
|
|
570
641
|
const root = this.store.taskRoot(identity.projectId, taskId);
|
|
571
642
|
const briefHash = this.store.hashArtifact(root, 'brief.md');
|
|
572
643
|
const planHash = this.store.hashArtifact(root, 'plan.md');
|
|
644
|
+
const delegation = this.delegatedAuthorization(identity.projectId, delegationGrantId, 'task.execution_authorize', actor, task);
|
|
573
645
|
const event = {
|
|
574
646
|
kind: 'execution',
|
|
575
647
|
decision: 'approved',
|
|
@@ -581,6 +653,8 @@ export class WorkflowService {
|
|
|
581
653
|
evidenceHash: null,
|
|
582
654
|
headCommit: null,
|
|
583
655
|
reason,
|
|
656
|
+
authorizationMode: delegation ? 'delegated' : 'human',
|
|
657
|
+
...(delegation ? { delegation } : {}),
|
|
584
658
|
};
|
|
585
659
|
return this.store.writeTask({ ...task, status: 'ready', briefHash, planHash, authorizations: [...task.authorizations, event] }, expectedRevision);
|
|
586
660
|
}
|
|
@@ -895,7 +969,7 @@ export class WorkflowService {
|
|
|
895
969
|
this.store.publishArtifact(stagedResult);
|
|
896
970
|
return saved;
|
|
897
971
|
}
|
|
898
|
-
acceptTask(repository, taskId, expectedRevision, actor) {
|
|
972
|
+
acceptTask(repository, taskId, expectedRevision, actor, delegationGrantId = null) {
|
|
899
973
|
const { identity } = this.context(repository);
|
|
900
974
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
901
975
|
assertExpectedRevision(task, expectedRevision);
|
|
@@ -910,6 +984,7 @@ export class WorkflowService {
|
|
|
910
984
|
this.assertReviewFresh(identity.repositoryRoot, task);
|
|
911
985
|
const resultHash = this.store.hashArtifact(root, 'result.md');
|
|
912
986
|
const evidenceHash = this.store.hashArtifact(root, 'evidence.json');
|
|
987
|
+
const delegation = this.delegatedAuthorization(identity.projectId, delegationGrantId, 'task.final_accept', actor, task);
|
|
913
988
|
const event = {
|
|
914
989
|
kind: 'final_acceptance',
|
|
915
990
|
decision: 'approved',
|
|
@@ -921,6 +996,8 @@ export class WorkflowService {
|
|
|
921
996
|
evidenceHash,
|
|
922
997
|
headCommit: headCommit(identity.repositoryRoot),
|
|
923
998
|
reason: '',
|
|
999
|
+
authorizationMode: delegation ? 'delegated' : 'human',
|
|
1000
|
+
...(delegation ? { delegation } : {}),
|
|
924
1001
|
};
|
|
925
1002
|
return this.store.writeTask({
|
|
926
1003
|
...task,
|
|
@@ -1024,6 +1101,7 @@ export class WorkflowService {
|
|
|
1024
1101
|
discoveries: this.store.listDiscoveries(identity.projectId),
|
|
1025
1102
|
tasks: this.store.listTasks(identity.projectId),
|
|
1026
1103
|
milestones: this.store.listMilestones(identity.projectId),
|
|
1104
|
+
delegations: this.store.listDelegations(identity.projectId),
|
|
1027
1105
|
};
|
|
1028
1106
|
}
|
|
1029
1107
|
scanKnowledgeMap(repository) {
|
|
@@ -1049,12 +1127,21 @@ export class WorkflowService {
|
|
|
1049
1127
|
const scanned = scanProjectKnowledge(identity.repositoryRoot, identity.projectId, this.now());
|
|
1050
1128
|
return inspectKnowledgeMap(current, scanned);
|
|
1051
1129
|
}
|
|
1052
|
-
approveKnowledgeMap(repository, expectedRevision, selections, actor) {
|
|
1130
|
+
approveKnowledgeMap(repository, expectedRevision, selections, actor, delegationGrantId = null) {
|
|
1053
1131
|
const { identity } = this.context(repository);
|
|
1054
1132
|
const current = this.store.readKnowledgeMap(identity.projectId);
|
|
1055
1133
|
assertExpectedRevision(current, expectedRevision);
|
|
1056
1134
|
const inspected = inspectKnowledgeMap(current, scanProjectKnowledge(identity.repositoryRoot, identity.projectId, this.now()));
|
|
1135
|
+
const delegation = this.delegatedAuthorization(identity.projectId, delegationGrantId, 'project_memory.approve', actor, inspected);
|
|
1057
1136
|
const approved = applyKnowledgeSelections(inspected, selections, actor, this.now());
|
|
1137
|
+
if (!approved.approval) {
|
|
1138
|
+
throw new WorkflowError('STATE_CORRUPT', 'Knowledge Map approval was not recorded.');
|
|
1139
|
+
}
|
|
1140
|
+
approved.approval = {
|
|
1141
|
+
...approved.approval,
|
|
1142
|
+
authorizationMode: delegation ? 'delegated' : 'human',
|
|
1143
|
+
...(delegation ? { delegation } : {}),
|
|
1144
|
+
};
|
|
1058
1145
|
return this.store.writeKnowledgeMap(approved, expectedRevision);
|
|
1059
1146
|
}
|
|
1060
1147
|
reconcileKnowledgeMap(repository, expectedRevision) {
|
|
@@ -1095,16 +1182,21 @@ export class WorkflowService {
|
|
|
1095
1182
|
const activeTask = status.tasks.find((task) => taskIsActionableFromRepositoryNext(task, milestonesById));
|
|
1096
1183
|
if (activeTask) {
|
|
1097
1184
|
if (!taskCanRebindKnowledge(activeTask) && activeTask.status !== 'planning') {
|
|
1098
|
-
return
|
|
1185
|
+
return this.nextForTaskWithDelegation(status.projectId, activeTask);
|
|
1099
1186
|
}
|
|
1100
1187
|
const knowledgeMap = this.knowledgeMapStatus(repository);
|
|
1101
1188
|
if (knowledgeMap.status !== 'active' || !knowledgeMap.approval) {
|
|
1189
|
+
const action = knowledgeMap.status === 'stale' ? 'project-memory reconcile' : 'project-memory approve';
|
|
1190
|
+
const delegatedApprovalOptions = action === 'project-memory approve'
|
|
1191
|
+
? this.delegatedApprovalOptions(status.projectId, 'project_memory.approve', knowledgeMap)
|
|
1192
|
+
: [];
|
|
1102
1193
|
return {
|
|
1103
1194
|
scope: 'project-memory',
|
|
1104
1195
|
status: knowledgeMap.status,
|
|
1105
|
-
action
|
|
1196
|
+
action,
|
|
1106
1197
|
revision: knowledgeMap.revision,
|
|
1107
1198
|
taskId: activeTask.id,
|
|
1199
|
+
...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
|
|
1108
1200
|
};
|
|
1109
1201
|
}
|
|
1110
1202
|
const knowledgeMapHash = hashKnowledgeMap(knowledgeMap);
|
|
@@ -1122,7 +1214,7 @@ export class WorkflowService {
|
|
|
1122
1214
|
knowledgeMapHash,
|
|
1123
1215
|
};
|
|
1124
1216
|
}
|
|
1125
|
-
return
|
|
1217
|
+
return this.nextForTaskWithDelegation(status.projectId, activeTask);
|
|
1126
1218
|
}
|
|
1127
1219
|
const activeMilestone = status.milestones.find((milestone) => !['accepted', 'cancelled'].includes(milestone.status));
|
|
1128
1220
|
if (activeMilestone) {
|
|
@@ -1149,21 +1241,29 @@ export class WorkflowService {
|
|
|
1149
1241
|
reason: 'final validation evidence is stale',
|
|
1150
1242
|
};
|
|
1151
1243
|
}
|
|
1244
|
+
const delegatedApprovalOptions = this.delegatedApprovalOptions(status.projectId, 'milestone.final_accept', activeMilestone);
|
|
1152
1245
|
return {
|
|
1153
1246
|
scope: 'milestone',
|
|
1154
1247
|
id: activeMilestone.id,
|
|
1155
1248
|
status: activeMilestone.status,
|
|
1156
|
-
action:
|
|
1249
|
+
action: delegatedApprovalOptions.length > 0
|
|
1250
|
+
? 'milestone accept using delegated approval or await explicit human final acceptance'
|
|
1251
|
+
: 'await explicit human final acceptance',
|
|
1157
1252
|
revision: activeMilestone.revision,
|
|
1158
1253
|
requiredHumanGate: milestoneFinalAcceptanceGate(activeMilestone, evidence.headCommit),
|
|
1254
|
+
...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
|
|
1159
1255
|
};
|
|
1160
1256
|
}
|
|
1257
|
+
const delegatedApprovalOptions = activeMilestone.status === 'awaiting_execution_authorization'
|
|
1258
|
+
? this.delegatedApprovalOptions(status.projectId, 'milestone.execution_authorize', activeMilestone)
|
|
1259
|
+
: [];
|
|
1161
1260
|
return {
|
|
1162
1261
|
scope: 'milestone',
|
|
1163
1262
|
id: activeMilestone.id,
|
|
1164
1263
|
status: activeMilestone.status,
|
|
1165
1264
|
action: nextForMilestone(activeMilestone),
|
|
1166
1265
|
revision: activeMilestone.revision,
|
|
1266
|
+
...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
|
|
1167
1267
|
};
|
|
1168
1268
|
}
|
|
1169
1269
|
const activeDiscovery = status.discoveries.find((discovery) => !['materialized', 'abandoned'].includes(discovery.status));
|
|
@@ -1178,6 +1278,51 @@ export class WorkflowService {
|
|
|
1178
1278
|
}
|
|
1179
1279
|
return { scope: 'repository', status: 'idle', action: 'discovery start' };
|
|
1180
1280
|
}
|
|
1281
|
+
delegatedAuthorization(projectId, grantId, transition, actor, target) {
|
|
1282
|
+
if (!grantId) {
|
|
1283
|
+
if (actor.trim().startsWith('agent:')) {
|
|
1284
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'An agent actor requires an explicit delegated approval grant.');
|
|
1285
|
+
}
|
|
1286
|
+
return null;
|
|
1287
|
+
}
|
|
1288
|
+
const grant = this.store.readDelegation(projectId, grantId);
|
|
1289
|
+
return assertDelegationCanAuthorize(grant, transition, actor, target, this.now());
|
|
1290
|
+
}
|
|
1291
|
+
nextForTaskWithDelegation(projectId, task) {
|
|
1292
|
+
const transition = task.status === 'awaiting_execution_authorization'
|
|
1293
|
+
? 'task.execution_authorize'
|
|
1294
|
+
: task.status === 'awaiting_final_acceptance'
|
|
1295
|
+
? 'task.final_accept'
|
|
1296
|
+
: null;
|
|
1297
|
+
const delegatedApprovalOptions = transition
|
|
1298
|
+
? this.delegatedApprovalOptions(projectId, transition, task)
|
|
1299
|
+
: [];
|
|
1300
|
+
return {
|
|
1301
|
+
...nextForTask(task),
|
|
1302
|
+
...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
delegatedApprovalOptions(projectId, transition, target) {
|
|
1306
|
+
return this.store.listDelegations(projectId)
|
|
1307
|
+
.filter((grant) => delegationCanAuthorize(grant, transition, target, this.now()))
|
|
1308
|
+
.map((grant) => ({
|
|
1309
|
+
grantId: grant.id,
|
|
1310
|
+
principal: grant.principal,
|
|
1311
|
+
delegate: grant.delegate,
|
|
1312
|
+
transition,
|
|
1313
|
+
scope: grant.scope,
|
|
1314
|
+
expiresAt: grant.expiresAt,
|
|
1315
|
+
policyHash: grant.policyHash,
|
|
1316
|
+
}));
|
|
1317
|
+
}
|
|
1318
|
+
assertDelegationScopeExists(projectId, policy) {
|
|
1319
|
+
if (policy.scope.kind === 'task') {
|
|
1320
|
+
this.store.readTask(projectId, policy.scope.id);
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1323
|
+
if (policy.scope.kind === 'milestone')
|
|
1324
|
+
this.store.readMilestone(projectId, policy.scope.id);
|
|
1325
|
+
}
|
|
1181
1326
|
assertExecutionAuthorizationFresh(task) {
|
|
1182
1327
|
const root = this.store.taskRoot(task.projectId, task.id);
|
|
1183
1328
|
const authorization = [...task.authorizations]
|