codex-workflow-v2 2.0.0-alpha.2 → 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 +24 -3
- package/dist/src/cli.js +25 -6
- package/dist/src/cli.js.map +1 -1
- package/dist/src/contracts.d.ts +70 -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 +242 -14
- package/dist/src/workflow.js.map +1 -1
- package/docs/decisions.md +15 -0
- package/docs/delegated-approval.md +164 -0
- package/docs/development-flow.md +27 -1
- package/docs/pdf/codex-workflow-v2-architecture-ru.pdf +336 -339
- package/docs/pdf/codex-workflow-v2-chat-only-guide-ru.pdf +761 -650
- package/docs/pdf/codex-workflow-v2-technical-reference-ru.pdf +502 -447
- package/docs/updating-existing-project.md +243 -0
- package/docs/validation-report.md +22 -6
- package/package.json +1 -1
- package/plugins/codex-workflow-gateway/references/protocol.md +30 -0
- package/plugins/codex-workflow-gateway/skills/codex-workflow-gateway/SKILL.md +36 -1
- package/references/state-machine.md +23 -1
- package/references/validation-and-review.md +5 -0
- package/roles/delivery-coordinator.md +5 -0
- package/schemas/authorization-event.schema.json +55 -0
- package/schemas/delegation-grant.schema.json +69 -0
package/dist/src/ulid.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ulid.js","sourceRoot":"","sources":["../../src/ulid.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1C,MAAM,QAAQ,GAAG,kCAAkC,CAAC;AAEpD,SAAS,MAAM,CAAC,KAAa,EAAE,MAAc;IAC3C,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,MAAM,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;QAC/C,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,KAAK,GAAG,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC;QAChD,KAAK,KAAK,EAAE,CAAC;IACf,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE;IACzC,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC;IAC1C,MAAM,MAAM,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC;IAC/B,IAAI,WAAW,GAAG,EAAE,CAAC;IACrB,KAAK,MAAM,IAAI,IAAI,MAAM;QAAE,WAAW,GAAG,CAAC,WAAW,IAAI,EAAE,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;IAC5E,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC,WAAW,EAAE,EAAE,CAAC,EAAE,CAAC;AAClD,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,
|
|
1
|
+
{"version":3,"file":"ulid.js","sourceRoot":"","sources":["../../src/ulid.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1C,MAAM,QAAQ,GAAG,kCAAkC,CAAC;AAEpD,SAAS,MAAM,CAAC,KAAa,EAAE,MAAc;IAC3C,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,MAAM,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;QAC/C,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,KAAK,GAAG,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC;QAChD,KAAK,KAAK,EAAE,CAAC;IACf,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE;IACzC,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC;IAC1C,MAAM,MAAM,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC;IAC/B,IAAI,WAAW,GAAG,EAAE,CAAC;IACrB,KAAK,MAAM,IAAI,IAAI,MAAM;QAAE,WAAW,GAAG,CAAC,WAAW,IAAI,EAAE,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;IAC5E,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC,WAAW,EAAE,EAAE,CAAC,EAAE,CAAC;AAClD,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,MAAsC,EAAE,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE;IACrF,OAAO,GAAG,MAAM,IAAI,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;AACxC,CAAC"}
|
package/dist/src/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export declare const PACKAGE_NAME = "codex-workflow-v2";
|
|
2
|
-
export declare const PACKAGE_VERSION = "2.0.0-alpha.
|
|
2
|
+
export declare const PACKAGE_VERSION = "2.0.0-alpha.4";
|
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): 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
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { appendFileSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { PROTOCOL_VERSION, STATE_SCHEMA_VERSION, } from './contracts.js';
|
|
4
5
|
import { renderBrief, renderPlan, renderResult } from './artifacts.js';
|
|
6
|
+
import { assertDelegationCanAuthorize, delegationCanAuthorize, prepareDelegationGrantGate, } from './delegation.js';
|
|
5
7
|
import { assessDiscovery, mergeUnique } from './domain/discovery.js';
|
|
6
8
|
import { pathAllowed, validatePlan } from './domain/validation.js';
|
|
7
9
|
import { WorkflowError } from './errors.js';
|
|
@@ -29,6 +31,70 @@ export class WorkflowService {
|
|
|
29
31
|
locks: new WriterLockManager(this.store.lockRoot(identity.projectId), this.now),
|
|
30
32
|
};
|
|
31
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
|
+
}
|
|
32
98
|
startDiscovery(repository, input) {
|
|
33
99
|
if (!input.goal.trim())
|
|
34
100
|
throw new WorkflowError('INVALID_ARGUMENT', 'Discovery requires a substantive goal.');
|
|
@@ -265,7 +331,7 @@ export class WorkflowService {
|
|
|
265
331
|
this.store.publishArtifact(stagedPlan);
|
|
266
332
|
return saved;
|
|
267
333
|
}
|
|
268
|
-
authorizeMilestone(repository, milestoneId, expectedRevision, actor, reason = '') {
|
|
334
|
+
authorizeMilestone(repository, milestoneId, expectedRevision, actor, reason = '', delegationGrantId = null) {
|
|
269
335
|
const { identity } = this.context(repository);
|
|
270
336
|
const milestone = this.store.readMilestone(identity.projectId, milestoneId);
|
|
271
337
|
assertExpectedRevision(milestone, expectedRevision);
|
|
@@ -277,6 +343,7 @@ export class WorkflowService {
|
|
|
277
343
|
if (planHash !== milestone.planHash) {
|
|
278
344
|
throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone Plan changed after it was recorded.');
|
|
279
345
|
}
|
|
346
|
+
const delegation = this.delegatedAuthorization(identity.projectId, delegationGrantId, 'milestone.execution_authorize', actor, milestone);
|
|
280
347
|
const event = {
|
|
281
348
|
kind: 'execution',
|
|
282
349
|
decision: 'approved',
|
|
@@ -288,6 +355,8 @@ export class WorkflowService {
|
|
|
288
355
|
evidenceHash: null,
|
|
289
356
|
headCommit: runGit(identity.repositoryRoot, ['rev-parse', milestone.baseBranch]),
|
|
290
357
|
reason,
|
|
358
|
+
authorizationMode: delegation ? 'delegated' : 'human',
|
|
359
|
+
...(delegation ? { delegation } : {}),
|
|
291
360
|
};
|
|
292
361
|
return this.store.writeMilestone({ ...milestone, status: 'active', planHash, authorizations: [...milestone.authorizations, event] }, expectedRevision);
|
|
293
362
|
}
|
|
@@ -295,8 +364,8 @@ export class WorkflowService {
|
|
|
295
364
|
const { identity } = this.context(repository);
|
|
296
365
|
const milestone = this.store.readMilestone(identity.projectId, milestoneId);
|
|
297
366
|
assertExpectedRevision(milestone, expectedRevision);
|
|
298
|
-
if (milestone.status
|
|
299
|
-
throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestoneId}
|
|
367
|
+
if (!['active', 'awaiting_final_acceptance'].includes(milestone.status)) {
|
|
368
|
+
throw new WorkflowError('TRANSITION_BLOCKED', `Milestone ${milestoneId} cannot be validated while ${milestone.status}.`);
|
|
300
369
|
}
|
|
301
370
|
this.assertMilestoneAuthorizationFresh(milestone);
|
|
302
371
|
if (currentBranch(identity.repositoryRoot) !== milestone.baseBranch || !isClean(identity.repositoryRoot)) {
|
|
@@ -354,7 +423,7 @@ export class WorkflowService {
|
|
|
354
423
|
this.store.publishArtifact(stagedResult);
|
|
355
424
|
return saved;
|
|
356
425
|
}
|
|
357
|
-
acceptMilestone(repository, milestoneId, expectedRevision, actor) {
|
|
426
|
+
acceptMilestone(repository, milestoneId, expectedRevision, actor, confirmationCode, delegationGrantId = null) {
|
|
358
427
|
const { identity } = this.context(repository);
|
|
359
428
|
const milestone = this.store.readMilestone(identity.projectId, milestoneId);
|
|
360
429
|
assertExpectedRevision(milestone, expectedRevision);
|
|
@@ -372,17 +441,31 @@ export class WorkflowService {
|
|
|
372
441
|
if (evidence.headCommit !== currentHead || resultHash !== milestone.resultHash || evidenceHash !== milestone.evidenceHash) {
|
|
373
442
|
throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone validation evidence is stale.');
|
|
374
443
|
}
|
|
444
|
+
const normalizedActor = actor.trim();
|
|
445
|
+
if (!normalizedActor) {
|
|
446
|
+
throw new WorkflowError('INVALID_ARGUMENT', 'Milestone final acceptance requires an explicit actor.');
|
|
447
|
+
}
|
|
448
|
+
const gate = milestoneFinalAcceptanceGate(milestone, currentHead);
|
|
449
|
+
if (confirmationCode.trim() !== gate.confirmationCode) {
|
|
450
|
+
throw new WorkflowError('TRANSITION_BLOCKED', 'Milestone final acceptance confirmation does not match the current validated state.', {
|
|
451
|
+
milestoneId,
|
|
452
|
+
revision: milestone.revision,
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
const delegation = this.delegatedAuthorization(identity.projectId, delegationGrantId, 'milestone.final_accept', normalizedActor, milestone);
|
|
375
456
|
const event = {
|
|
376
457
|
kind: 'final_acceptance',
|
|
377
458
|
decision: 'approved',
|
|
378
|
-
actor:
|
|
459
|
+
actor: normalizedActor,
|
|
379
460
|
recordedAt: this.now().toISOString(),
|
|
380
461
|
briefHash: null,
|
|
381
462
|
planHash: milestone.planHash,
|
|
382
463
|
resultHash,
|
|
383
464
|
evidenceHash,
|
|
384
465
|
headCommit: currentHead,
|
|
385
|
-
reason:
|
|
466
|
+
reason: `Confirmed with ${gate.confirmationCode}.`,
|
|
467
|
+
authorizationMode: delegation ? 'delegated' : 'human',
|
|
468
|
+
...(delegation ? { delegation } : {}),
|
|
386
469
|
};
|
|
387
470
|
return this.store.writeMilestone({
|
|
388
471
|
...milestone,
|
|
@@ -547,7 +630,7 @@ export class WorkflowService {
|
|
|
547
630
|
this.store.publishArtifact(stagedPlan);
|
|
548
631
|
return saved;
|
|
549
632
|
}
|
|
550
|
-
authorizeTask(repository, taskId, expectedRevision, actor, reason = '') {
|
|
633
|
+
authorizeTask(repository, taskId, expectedRevision, actor, reason = '', delegationGrantId = null) {
|
|
551
634
|
const { identity } = this.context(repository);
|
|
552
635
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
553
636
|
assertExpectedRevision(task, expectedRevision);
|
|
@@ -558,6 +641,7 @@ export class WorkflowService {
|
|
|
558
641
|
const root = this.store.taskRoot(identity.projectId, taskId);
|
|
559
642
|
const briefHash = this.store.hashArtifact(root, 'brief.md');
|
|
560
643
|
const planHash = this.store.hashArtifact(root, 'plan.md');
|
|
644
|
+
const delegation = this.delegatedAuthorization(identity.projectId, delegationGrantId, 'task.execution_authorize', actor, task);
|
|
561
645
|
const event = {
|
|
562
646
|
kind: 'execution',
|
|
563
647
|
decision: 'approved',
|
|
@@ -569,6 +653,8 @@ export class WorkflowService {
|
|
|
569
653
|
evidenceHash: null,
|
|
570
654
|
headCommit: null,
|
|
571
655
|
reason,
|
|
656
|
+
authorizationMode: delegation ? 'delegated' : 'human',
|
|
657
|
+
...(delegation ? { delegation } : {}),
|
|
572
658
|
};
|
|
573
659
|
return this.store.writeTask({ ...task, status: 'ready', briefHash, planHash, authorizations: [...task.authorizations, event] }, expectedRevision);
|
|
574
660
|
}
|
|
@@ -883,7 +969,7 @@ export class WorkflowService {
|
|
|
883
969
|
this.store.publishArtifact(stagedResult);
|
|
884
970
|
return saved;
|
|
885
971
|
}
|
|
886
|
-
acceptTask(repository, taskId, expectedRevision, actor) {
|
|
972
|
+
acceptTask(repository, taskId, expectedRevision, actor, delegationGrantId = null) {
|
|
887
973
|
const { identity } = this.context(repository);
|
|
888
974
|
const task = this.store.readTask(identity.projectId, taskId);
|
|
889
975
|
assertExpectedRevision(task, expectedRevision);
|
|
@@ -898,6 +984,7 @@ export class WorkflowService {
|
|
|
898
984
|
this.assertReviewFresh(identity.repositoryRoot, task);
|
|
899
985
|
const resultHash = this.store.hashArtifact(root, 'result.md');
|
|
900
986
|
const evidenceHash = this.store.hashArtifact(root, 'evidence.json');
|
|
987
|
+
const delegation = this.delegatedAuthorization(identity.projectId, delegationGrantId, 'task.final_accept', actor, task);
|
|
901
988
|
const event = {
|
|
902
989
|
kind: 'final_acceptance',
|
|
903
990
|
decision: 'approved',
|
|
@@ -909,6 +996,8 @@ export class WorkflowService {
|
|
|
909
996
|
evidenceHash,
|
|
910
997
|
headCommit: headCommit(identity.repositoryRoot),
|
|
911
998
|
reason: '',
|
|
999
|
+
authorizationMode: delegation ? 'delegated' : 'human',
|
|
1000
|
+
...(delegation ? { delegation } : {}),
|
|
912
1001
|
};
|
|
913
1002
|
return this.store.writeTask({
|
|
914
1003
|
...task,
|
|
@@ -1012,6 +1101,7 @@ export class WorkflowService {
|
|
|
1012
1101
|
discoveries: this.store.listDiscoveries(identity.projectId),
|
|
1013
1102
|
tasks: this.store.listTasks(identity.projectId),
|
|
1014
1103
|
milestones: this.store.listMilestones(identity.projectId),
|
|
1104
|
+
delegations: this.store.listDelegations(identity.projectId),
|
|
1015
1105
|
};
|
|
1016
1106
|
}
|
|
1017
1107
|
scanKnowledgeMap(repository) {
|
|
@@ -1037,12 +1127,21 @@ export class WorkflowService {
|
|
|
1037
1127
|
const scanned = scanProjectKnowledge(identity.repositoryRoot, identity.projectId, this.now());
|
|
1038
1128
|
return inspectKnowledgeMap(current, scanned);
|
|
1039
1129
|
}
|
|
1040
|
-
approveKnowledgeMap(repository, expectedRevision, selections, actor) {
|
|
1130
|
+
approveKnowledgeMap(repository, expectedRevision, selections, actor, delegationGrantId = null) {
|
|
1041
1131
|
const { identity } = this.context(repository);
|
|
1042
1132
|
const current = this.store.readKnowledgeMap(identity.projectId);
|
|
1043
1133
|
assertExpectedRevision(current, expectedRevision);
|
|
1044
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);
|
|
1045
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
|
+
};
|
|
1046
1145
|
return this.store.writeKnowledgeMap(approved, expectedRevision);
|
|
1047
1146
|
}
|
|
1048
1147
|
reconcileKnowledgeMap(repository, expectedRevision) {
|
|
@@ -1078,19 +1177,26 @@ export class WorkflowService {
|
|
|
1078
1177
|
}
|
|
1079
1178
|
next(repository) {
|
|
1080
1179
|
const status = this.status(repository);
|
|
1081
|
-
const
|
|
1180
|
+
const repositoryRoot = resolveRepositoryIdentity(repository).repositoryRoot;
|
|
1181
|
+
const milestonesById = new Map(status.milestones.map((milestone) => [milestone.id, milestone]));
|
|
1182
|
+
const activeTask = status.tasks.find((task) => taskIsActionableFromRepositoryNext(task, milestonesById));
|
|
1082
1183
|
if (activeTask) {
|
|
1083
1184
|
if (!taskCanRebindKnowledge(activeTask) && activeTask.status !== 'planning') {
|
|
1084
|
-
return
|
|
1185
|
+
return this.nextForTaskWithDelegation(status.projectId, activeTask);
|
|
1085
1186
|
}
|
|
1086
1187
|
const knowledgeMap = this.knowledgeMapStatus(repository);
|
|
1087
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
|
+
: [];
|
|
1088
1193
|
return {
|
|
1089
1194
|
scope: 'project-memory',
|
|
1090
1195
|
status: knowledgeMap.status,
|
|
1091
|
-
action
|
|
1196
|
+
action,
|
|
1092
1197
|
revision: knowledgeMap.revision,
|
|
1093
1198
|
taskId: activeTask.id,
|
|
1199
|
+
...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
|
|
1094
1200
|
};
|
|
1095
1201
|
}
|
|
1096
1202
|
const knowledgeMapHash = hashKnowledgeMap(knowledgeMap);
|
|
@@ -1108,16 +1214,56 @@ export class WorkflowService {
|
|
|
1108
1214
|
knowledgeMapHash,
|
|
1109
1215
|
};
|
|
1110
1216
|
}
|
|
1111
|
-
return
|
|
1217
|
+
return this.nextForTaskWithDelegation(status.projectId, activeTask);
|
|
1112
1218
|
}
|
|
1113
1219
|
const activeMilestone = status.milestones.find((milestone) => !['accepted', 'cancelled'].includes(milestone.status));
|
|
1114
1220
|
if (activeMilestone) {
|
|
1221
|
+
if (activeMilestone.status === 'awaiting_final_acceptance') {
|
|
1222
|
+
const root = this.store.milestoneRoot(activeMilestone.projectId, activeMilestone.id);
|
|
1223
|
+
const evidence = JSON.parse(readFileSync(path.join(root, 'evidence.json'), 'utf8'));
|
|
1224
|
+
if (!evidence.headCommit || !activeMilestone.resultHash || !activeMilestone.evidenceHash) {
|
|
1225
|
+
throw new WorkflowError('STATE_CORRUPT', `Milestone ${activeMilestone.id} has incomplete final evidence.`);
|
|
1226
|
+
}
|
|
1227
|
+
const currentHead = headCommit(repositoryRoot);
|
|
1228
|
+
const resultHash = this.store.hashArtifact(root, 'result.md');
|
|
1229
|
+
const evidenceHash = this.store.hashArtifact(root, 'evidence.json');
|
|
1230
|
+
if (currentBranch(repositoryRoot) !== activeMilestone.baseBranch ||
|
|
1231
|
+
!isClean(repositoryRoot) ||
|
|
1232
|
+
evidence.headCommit !== currentHead ||
|
|
1233
|
+
resultHash !== activeMilestone.resultHash ||
|
|
1234
|
+
evidenceHash !== activeMilestone.evidenceHash) {
|
|
1235
|
+
return {
|
|
1236
|
+
scope: 'milestone',
|
|
1237
|
+
id: activeMilestone.id,
|
|
1238
|
+
status: activeMilestone.status,
|
|
1239
|
+
action: 'restore clean base, then milestone validate',
|
|
1240
|
+
revision: activeMilestone.revision,
|
|
1241
|
+
reason: 'final validation evidence is stale',
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
const delegatedApprovalOptions = this.delegatedApprovalOptions(status.projectId, 'milestone.final_accept', activeMilestone);
|
|
1245
|
+
return {
|
|
1246
|
+
scope: 'milestone',
|
|
1247
|
+
id: activeMilestone.id,
|
|
1248
|
+
status: activeMilestone.status,
|
|
1249
|
+
action: delegatedApprovalOptions.length > 0
|
|
1250
|
+
? 'milestone accept using delegated approval or await explicit human final acceptance'
|
|
1251
|
+
: 'await explicit human final acceptance',
|
|
1252
|
+
revision: activeMilestone.revision,
|
|
1253
|
+
requiredHumanGate: milestoneFinalAcceptanceGate(activeMilestone, evidence.headCommit),
|
|
1254
|
+
...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
1257
|
+
const delegatedApprovalOptions = activeMilestone.status === 'awaiting_execution_authorization'
|
|
1258
|
+
? this.delegatedApprovalOptions(status.projectId, 'milestone.execution_authorize', activeMilestone)
|
|
1259
|
+
: [];
|
|
1115
1260
|
return {
|
|
1116
1261
|
scope: 'milestone',
|
|
1117
1262
|
id: activeMilestone.id,
|
|
1118
1263
|
status: activeMilestone.status,
|
|
1119
1264
|
action: nextForMilestone(activeMilestone),
|
|
1120
1265
|
revision: activeMilestone.revision,
|
|
1266
|
+
...(delegatedApprovalOptions.length > 0 ? { delegatedApprovalOptions } : {}),
|
|
1121
1267
|
};
|
|
1122
1268
|
}
|
|
1123
1269
|
const activeDiscovery = status.discoveries.find((discovery) => !['materialized', 'abandoned'].includes(discovery.status));
|
|
@@ -1132,6 +1278,51 @@ export class WorkflowService {
|
|
|
1132
1278
|
}
|
|
1133
1279
|
return { scope: 'repository', status: 'idle', action: 'discovery start' };
|
|
1134
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
|
+
}
|
|
1135
1326
|
assertExecutionAuthorizationFresh(task) {
|
|
1136
1327
|
const root = this.store.taskRoot(task.projectId, task.id);
|
|
1137
1328
|
const authorization = [...task.authorizations]
|
|
@@ -1363,13 +1554,50 @@ function taskCanRebindKnowledge(task) {
|
|
|
1363
1554
|
'awaiting_final_acceptance',
|
|
1364
1555
|
].includes(task.status);
|
|
1365
1556
|
}
|
|
1557
|
+
function taskIsActionableFromRepositoryNext(task, milestonesById) {
|
|
1558
|
+
if (task.status === 'merged' || task.status === 'cancelled')
|
|
1559
|
+
return false;
|
|
1560
|
+
if (!task.milestoneId)
|
|
1561
|
+
return true;
|
|
1562
|
+
const milestone = milestonesById.get(task.milestoneId);
|
|
1563
|
+
if (!milestone)
|
|
1564
|
+
return false;
|
|
1565
|
+
if (taskHasStarted(task))
|
|
1566
|
+
return !['accepted', 'cancelled'].includes(milestone.status);
|
|
1567
|
+
const membership = milestone.memberships.find((candidate) => candidate.taskId === task.id);
|
|
1568
|
+
return milestone.status === 'active' && membership?.disposition === 'required';
|
|
1569
|
+
}
|
|
1570
|
+
function milestoneFinalAcceptanceGate(milestone, headCommit) {
|
|
1571
|
+
if (!milestone.resultHash || !milestone.evidenceHash) {
|
|
1572
|
+
throw new WorkflowError('STATE_CORRUPT', `Milestone ${milestone.id} has no final acceptance artifacts.`);
|
|
1573
|
+
}
|
|
1574
|
+
const binding = [
|
|
1575
|
+
'milestone-final-acceptance-v1',
|
|
1576
|
+
milestone.id,
|
|
1577
|
+
String(milestone.revision),
|
|
1578
|
+
milestone.planHash ?? '',
|
|
1579
|
+
milestone.resultHash,
|
|
1580
|
+
milestone.evidenceHash,
|
|
1581
|
+
headCommit,
|
|
1582
|
+
].join('\0');
|
|
1583
|
+
return {
|
|
1584
|
+
kind: 'milestone_final_acceptance',
|
|
1585
|
+
milestoneId: milestone.id,
|
|
1586
|
+
revision: milestone.revision,
|
|
1587
|
+
planHash: milestone.planHash,
|
|
1588
|
+
resultHash: milestone.resultHash,
|
|
1589
|
+
evidenceHash: milestone.evidenceHash,
|
|
1590
|
+
headCommit,
|
|
1591
|
+
confirmationCode: `MSA-${createHash('sha256').update(binding).digest('hex').slice(0, 24).toUpperCase()}`,
|
|
1592
|
+
};
|
|
1593
|
+
}
|
|
1366
1594
|
function nextForMilestone(milestone) {
|
|
1367
1595
|
const actions = {
|
|
1368
1596
|
planning: 'milestone plan-set',
|
|
1369
1597
|
awaiting_execution_authorization: 'milestone authorize',
|
|
1370
1598
|
active: 'complete required Tasks, then milestone validate',
|
|
1371
1599
|
validating: 'milestone validate',
|
|
1372
|
-
awaiting_final_acceptance: '
|
|
1600
|
+
awaiting_final_acceptance: 'await explicit human final acceptance',
|
|
1373
1601
|
accepted: 'none',
|
|
1374
1602
|
blocked: 'milestone plan-set or milestone cancel',
|
|
1375
1603
|
cancelled: 'none',
|