@williambeto/ai-workflow 2.2.7 → 2.3.0

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.
@@ -2,15 +2,89 @@ import { RequestClassifier } from "../core/request-classifier.js";
2
2
  import { ExecutionPlanner } from "../core/execution-planner.js";
3
3
  import { WorkflowStateMachine } from "../core/workflow-state-machine.js";
4
4
  import { BranchGate } from "../core/gates/branch-gate.js";
5
+ import { EvidenceLedger } from "../core/evidence/evidence-ledger.js";
6
+ import { DelegationController } from "../core/delegation-controller.js";
5
7
  import { OpenCodeAdapter } from "../core/runtime/opencode-adapter.js";
6
8
  import { runCollectEvidence } from "./collect-evidence.js";
7
9
  import { QualityGuard } from "../core/validation/quality-guard.js";
8
10
  import { HandoffEngine } from "../core/handoff/handoff-engine.js";
9
11
  import { HealerEngine } from "../core/healing/healer-engine.js";
10
- import { createCliRemediationExecutor } from "../core/healing/cli-remediation-executor.js";
12
+ import { createRuntimeRemediationExecutor } from "../core/healing/runtime-remediation-executor.js";
11
13
  import { isRecoverableGateFailure, isTerminalFailure } from "../core/statuses.js";
14
+ import { Finalizer } from "../core/finalization/finalizer.js";
12
15
  import fs from "node:fs/promises";
13
16
  import path from "node:path";
17
+ import { execSync } from "node:child_process";
18
+
19
+ async function captureReadOnlyState(cwd) {
20
+ const finalizer = new Finalizer({ cwd });
21
+ const snapshot = await finalizer.snapshotManager.capture();
22
+
23
+ const execGit = (args) => {
24
+ try {
25
+ return execSync(`git ${args}`, { cwd, encoding: "utf8", stdio: "pipe" }).trim();
26
+ } catch {
27
+ return "";
28
+ }
29
+ };
30
+
31
+ const head = execGit("rev-parse HEAD");
32
+ const branch = execGit("rev-parse --abbrev-ref HEAD");
33
+ const indexTree = execGit("write-tree");
34
+ const porcelainStatus = execGit("status --porcelain");
35
+
36
+ return {
37
+ snapshot,
38
+ head,
39
+ branch,
40
+ indexTree,
41
+ porcelainStatus
42
+ };
43
+ }
44
+
45
+ function compareReadOnlyState(before, after) {
46
+ const violations = [];
47
+
48
+ // 1. Compare branch
49
+ if (before.branch !== after.branch) {
50
+ violations.push(`Branch changed from '${before.branch}' to '${after.branch}'`);
51
+ }
52
+
53
+ // 2. Compare HEAD
54
+ if (before.head !== after.head) {
55
+ violations.push(`Git HEAD changed from '${before.head}' to '${after.head}' (commit or reset made inside read-only task)`);
56
+ }
57
+
58
+ // 3. Compare index tree
59
+ if (before.indexTree !== after.indexTree) {
60
+ violations.push(`Git index tree changed from '${before.indexTree}' to '${after.indexTree}'`);
61
+ }
62
+
63
+ // 4. Compare porcelain status
64
+ if (before.porcelainStatus !== after.porcelainStatus) {
65
+ violations.push(`Git status changed. Before:\n${before.porcelainStatus}\nAfter:\n${after.porcelainStatus}`);
66
+ }
67
+
68
+ // 5. Compare workspace files (SHA-256 hashes)
69
+ const beforeFiles = before.snapshot.files || {};
70
+ const afterFiles = after.snapshot.files || {};
71
+
72
+ for (const [file, info] of Object.entries(afterFiles)) {
73
+ const prevInfo = beforeFiles[file];
74
+ if (!prevInfo) {
75
+ violations.push(`File added: ${file}`);
76
+ } else if (prevInfo.sha256 !== info.sha256) {
77
+ violations.push(`File content modified: ${file}`);
78
+ }
79
+ }
80
+ for (const file of Object.keys(beforeFiles)) {
81
+ if (!afterFiles[file]) {
82
+ violations.push(`File deleted: ${file}`);
83
+ }
84
+ }
85
+
86
+ return violations;
87
+ }
14
88
 
15
89
  function slugify(text) {
16
90
  return text
@@ -34,7 +108,7 @@ function combineValidation(evidence, quality) {
34
108
  /**
35
109
  * runExecute - Coordinators natural request execution.
36
110
  */
37
- export async function runExecute({ cwd, naturalRequest, override, taskSlug: taskSlugOverride }) {
111
+ export async function runExecute({ cwd, naturalRequest, taskSlug: taskSlugOverride }) {
38
112
  if (!naturalRequest || !naturalRequest.trim()) {
39
113
  throw new Error("Missing request. Please provide a natural request string via positional arguments or --request flag.");
40
114
  }
@@ -42,131 +116,241 @@ export async function runExecute({ cwd, naturalRequest, override, taskSlug: task
42
116
  const taskSlug = taskSlugOverride || slugify(naturalRequest);
43
117
  console.log(`\n[AI WORKFLOW] Executing natural request intake [slug: ${taskSlug}]...\n`);
44
118
 
45
- const stateMachine = new WorkflowStateMachine();
46
-
47
- // 1. Classification
48
- const classifier = new RequestClassifier();
49
- const classification = classifier.classify(naturalRequest);
50
- console.log(`[CLASSIFIED] Intent: ${classification.intent}, Mode: ${classification.mode}, Profile: ${classification.profile}`);
51
- stateMachine.transitionTo("CLASSIFIED");
52
-
53
- // 2. Planning
54
- const planner = new ExecutionPlanner({ cwd });
55
- const plan = planner.plan(classification, taskSlug);
56
- console.log(`[PLANNED] Owner: ${plan.owner}, Remediation limit: ${plan.remediationLimit}`);
57
- stateMachine.transitionTo("PLANNED");
58
-
59
- // 3. Branch Gate
60
- const branchGate = new BranchGate({ memoryDir: path.join(cwd, ".ai-workflow"), cwd });
61
- const gateResult = branchGate.check(override, {
62
- autoRecover: true,
63
- taskSlug,
64
- readOnly: !plan.branchNeeded
65
- });
66
-
67
- if (gateResult.blocked) {
68
- stateMachine.transitionTo("BLOCKED");
69
- throw new Error(`[GATE BLOCKED] ${gateResult.reason}`);
70
- }
71
- console.log(`[PASS] Branch Gate: ${gateResult.recovered ? `${gateResult.branchBefore} -> ${gateResult.branch}` : `${gateResult.branch} is authorized`}.`);
72
- stateMachine.transitionTo("BRANCH_READY");
73
-
74
- // 4. Spec template creation (for full/deep mode)
75
- if (plan.specPath) {
76
- const fullSpecPath = path.join(cwd, plan.specPath);
77
- const specExists = await fs.access(fullSpecPath).then(() => true).catch(() => false);
78
- if (!specExists) {
79
- await fs.mkdir(path.dirname(fullSpecPath), { recursive: true });
80
- const specTemplatePath = path.join(cwd, ".ai-workflow/templates/specs/standard.md");
81
- const templateContent = await fs.readFile(specTemplatePath, "utf8").catch(() => {
82
- return `# [STANDARD] Specification: ${classification.request}\n\n## Metadata\n\n- ID: SPEC-${taskSlug}\n- Author: Spec-Engineer\n- Status: DRAFT\n- Date: ${new Date().toISOString().split("T")[0]}\n\n## Functional Requirements\n\n- ${classification.request}\n\n## Technical Implementation Plan\n\n### Files to Create/Modify\n\n- \`src/index.js\`\n\n## Acceptance Criteria\n\n- [ ] Implemented successfully\n\n## Testing Strategy\n\n- [ ] Behavior tests pass`;
83
- });
84
- await fs.writeFile(fullSpecPath, templateContent);
85
- console.log(`[EXECUTE] Created DRAFT specification template at: ${plan.specPath}`);
86
- }
87
- }
119
+ const ledger = new EvidenceLedger({ cwd, workflowId: taskSlug });
120
+ const delegationController = new DelegationController({ cwd, ledger, adapter: new OpenCodeAdapter({ cwd }) });
88
121
 
89
- // 5. Delegation
90
- stateMachine.transitionTo("DELEGATED");
91
- stateMachine.transitionTo("IMPLEMENTING");
122
+ let plan = null;
123
+ try {
124
+ const stateMachine = new WorkflowStateMachine();
92
125
 
93
- const adapter = new OpenCodeAdapter({ cwd });
94
- let promptMsg = classification.request;
95
- if (plan.specPath) {
96
- promptMsg = `Please review and fill in the specification file at: ${plan.specPath}. Make sure to change the Status field to 'APPROVED' in the Metadata section, and then implement the behavior described.`;
97
- }
126
+ // 1. Classification
127
+ const classifier = new RequestClassifier();
128
+ const classification = classifier.classify(naturalRequest);
129
+ console.log(`[CLASSIFIED] Intent: ${classification.intent}, Mode: ${classification.mode}, Profile: ${classification.profile}`);
130
+ stateMachine.transitionTo("CLASSIFIED");
98
131
 
99
- const runResult = await adapter.execute(promptMsg, {
100
- agent: plan.owner,
101
- dangerouslySkipPermissions: true
102
- });
132
+ await delegationController.route(naturalRequest, classification);
103
133
 
104
- if (!runResult.success) {
105
- console.error(`[EXECUTE] OpenCode adapter execution reported failure.`);
106
- }
107
- stateMachine.transitionTo("IMPLEMENTED");
108
-
109
- // 6. Validation
110
- stateMachine.transitionTo("VALIDATING");
111
- const validateWorkflow = async () => {
112
- const evidence = await runCollectEvidence({
113
- cwd,
114
- exitOnError: false,
134
+ // 2. Planning
135
+ const planner = new ExecutionPlanner({ cwd });
136
+ plan = planner.plan(classification, taskSlug);
137
+ console.log(`[PLANNED] Owner: ${plan.owner}, Remediation limit: ${plan.remediationLimit}`);
138
+ stateMachine.transitionTo("PLANNED");
139
+
140
+ // 3. Branch Gate
141
+ const branchGate = new BranchGate({ memoryDir: path.join(cwd, ".ai-workflow"), cwd });
142
+ const gateResult = branchGate.check(undefined, {
143
+ autoRecover: true,
115
144
  taskSlug,
116
- mode: plan.mode,
117
- profile: plan.profile,
118
- branchRecovery: gateResult.recovered ? `${gateResult.branchBefore} -> ${gateResult.branch}` : "NOT_REQUIRED"
145
+ readOnly: !plan.branchNeeded
119
146
  });
120
- const quality = await new QualityGuard({ cwd, taskSlug, mode: plan.mode }).verify();
121
- return combineValidation(evidence, quality);
122
- };
123
147
 
124
- let result = await validateWorkflow();
125
-
126
- // 7. Bounded Remediation
127
- if (isRecoverableGateFailure(result.overallStatus)) {
128
- stateMachine.transitionTo("REMEDIATING");
129
- console.log(`\n[REMEDIATION REQUIRED] ${result.overallStatus}. Starting bounded ${plan.mode} remediation.`);
130
- const executor = createCliRemediationExecutor(cwd);
131
- const healer = new HealerEngine({ cwd, mode: plan.mode, taskSlug });
132
-
133
- result = await healer.run({
134
- initialResult: result,
135
- validate: async () => {
136
- stateMachine.transitionTo("REVALIDATING");
137
- const res = await validateWorkflow();
138
- return res;
139
- },
140
- remediate: executor
148
+ if (gateResult.blocked) {
149
+ stateMachine.transitionTo("BLOCKED");
150
+ throw new Error(`[GATE BLOCKED] ${gateResult.reason}`);
151
+ }
152
+ console.log(`[PASS] Branch Gate: ${gateResult.recovered ? `${gateResult.branchBefore} -> ${gateResult.branch}` : `${gateResult.branch} is authorized`}.`);
153
+ stateMachine.transitionTo("BRANCH_READY");
154
+
155
+ const checkBranchSafety = () => {
156
+ if (plan.branchNeeded) {
157
+ const currentBranch = branchGate.getCurrentBranch();
158
+ if (branchGate.protectedBranches.includes(currentBranch)) {
159
+ stateMachine.transitionTo("BLOCKED");
160
+ throw new Error(`[WORKFLOW BLOCKED] Security violation: current branch is protected '${currentBranch}'!`);
161
+ }
162
+ }
163
+ };
164
+
165
+ // 4. Spec template creation (for full/deep mode)
166
+ if (plan.specPath) {
167
+ const fullSpecPath = path.join(cwd, plan.specPath);
168
+ const specExists = await fs.access(fullSpecPath).then(() => true).catch(() => false);
169
+ if (!specExists) {
170
+ await fs.mkdir(path.dirname(fullSpecPath), { recursive: true });
171
+ const specTemplatePath = path.join(cwd, ".ai-workflow/templates/specs/standard.md");
172
+ const templateContent = await fs.readFile(specTemplatePath, "utf8").catch(() => {
173
+ return `# [STANDARD] Specification: ${classification.request}\n\n## Metadata\n\n- ID: SPEC-${taskSlug}\n- Author: Spec-Engineer\n- Status: DRAFT\n- Date: ${new Date().toISOString().split("T")[0]}\n\n## Functional Requirements\n\n- ${classification.request}\n\n## Technical Implementation Plan\n\n### Files to Create/Modify\n\n- \`src/index.js\`\n\n## Acceptance Criteria\n\n- [ ] Implemented successfully\n\n## Testing Strategy\n\n- [ ] Behavior tests pass`;
174
+ });
175
+ await fs.writeFile(fullSpecPath, templateContent);
176
+ console.log(`[EXECUTE] Created DRAFT specification template at: ${plan.specPath}`);
177
+ }
178
+ }
179
+
180
+ // 5. Delegation
181
+ stateMachine.transitionTo("DELEGATED");
182
+ stateMachine.transitionTo("IMPLEMENTING");
183
+
184
+ let promptMsg = classification.request;
185
+ if (plan.specPath) {
186
+ promptMsg = `Please review and fill in the specification file at: ${plan.specPath}. Make sure to change the Status field to 'APPROVED' in the Metadata section, and then implement the behavior described.`;
187
+ }
188
+
189
+ let readOnlyStateBefore = null;
190
+ if (!plan.branchNeeded) {
191
+ readOnlyStateBefore = await captureReadOnlyState(cwd);
192
+ }
193
+
194
+ const runResult = await delegationController.implement(promptMsg, plan.owner, { readOnly: !plan.branchNeeded });
195
+
196
+ if (!runResult.success) {
197
+ console.error(`[EXECUTE] OpenCode adapter execution reported failure: ${runResult.error || "Unknown error"}`);
198
+ stateMachine.transitionTo("BLOCKED");
199
+ throw new Error(`[WORKFLOW BLOCKED] OpenCode runtime execution failed: ${runResult.error || "Unknown error"}`);
200
+ }
201
+
202
+ if (!plan.branchNeeded) {
203
+ const readOnlyStateAfter = await captureReadOnlyState(cwd);
204
+ const violations = compareReadOnlyState(readOnlyStateBefore, readOnlyStateAfter);
205
+ if (violations.length > 0) {
206
+ console.error(`[EXECUTE] Read-only confinement violation detected:`);
207
+ for (const v of violations) {
208
+ console.error(` - ${v}`);
209
+ }
210
+ stateMachine.transitionTo("BLOCKED");
211
+ throw new Error(`[WORKFLOW BLOCKED] Read-only confinement violation: files were written/modified, index or commits modified: \n${violations.join("\n")}`);
212
+ }
213
+
214
+ stateMachine.transitionTo("IMPLEMENTED");
215
+ stateMachine.transitionTo("VALIDATING");
216
+ stateMachine.transitionTo("COMPLETED");
217
+
218
+ console.log("\n--- Final Handoff ---");
219
+ console.log(`\n[AI WORKFLOW COMPLETE] COMPLETED`);
220
+ console.log(`Handoff Packet: [IN-MEMORY] (No handoff file written in read-only mode)\n`);
221
+
222
+ return {
223
+ overallStatus: "PASS",
224
+ evidence: {
225
+ internalStatus: "PASS",
226
+ commandsRun: []
227
+ },
228
+ quality: {
229
+ overallStatus: "PASS"
230
+ },
231
+ stateHistory: stateMachine.getHistory()
232
+ };
233
+ }
234
+
235
+ stateMachine.transitionTo("IMPLEMENTED");
236
+ checkBranchSafety();
237
+
238
+ // Capture snapshot immediately after last implementation write
239
+ const finalizer = new Finalizer({ cwd });
240
+ let lastWriteSnapshot = await finalizer.snapshotManager.capture();
241
+
242
+ // 6. Validation
243
+ stateMachine.transitionTo("VALIDATING");
244
+ const validateWorkflow = async () => {
245
+ const evidence = await runCollectEvidence({
246
+ cwd,
247
+ exitOnError: false,
248
+ taskSlug,
249
+ mode: plan.mode,
250
+ profile: plan.profile,
251
+ branchRecovery: gateResult.recovered ? `${gateResult.branchBefore} -> ${gateResult.branch}` : "NOT_REQUIRED"
252
+ });
253
+ const quality = await new QualityGuard({ cwd, taskSlug, mode: plan.mode }).verify();
254
+ return combineValidation(evidence, quality);
255
+ };
256
+
257
+ let result = await delegationController.validate(taskSlug, plan.mode, plan.profile, validateWorkflow);
258
+
259
+ // 7. Bounded Remediation
260
+ if (isRecoverableGateFailure(result.overallStatus)) {
261
+ console.log(`\n[REMEDIATION REQUIRED] ${result.overallStatus}. Starting bounded ${plan.mode} remediation.`);
262
+ const executor = createRuntimeRemediationExecutor(cwd, ledger);
263
+ const healer = new HealerEngine({ cwd, mode: plan.mode, taskSlug, ledger });
264
+
265
+ result = await healer.run({
266
+ initialResult: result,
267
+ validate: async () => {
268
+ stateMachine.transitionTo("REVALIDATING");
269
+ const res = await delegationController.validate(taskSlug, plan.mode, plan.profile, validateWorkflow);
270
+ return res;
271
+ },
272
+ remediate: async (remedCtx) => {
273
+ stateMachine.transitionTo("REMEDIATING");
274
+ const remRes = await executor(remedCtx);
275
+ if (remRes.applied) {
276
+ checkBranchSafety();
277
+ // Update snapshot because remediation wrote new changes
278
+ lastWriteSnapshot = await finalizer.snapshotManager.capture();
279
+ }
280
+ return remRes;
281
+ }
282
+ });
283
+ }
284
+
285
+ if (isTerminalFailure(result.overallStatus)) {
286
+ stateMachine.transitionTo("BLOCKED");
287
+ throw new Error(`[WORKFLOW BLOCKED] ${result.overallStatus}: Validation could not be resolved safely.`);
288
+ }
289
+
290
+ checkBranchSafety();
291
+
292
+ // 8. Finalizer / Stabilization Loop
293
+ let integrity = await finalizer.verifyIntegrity(lastWriteSnapshot);
294
+ let revalidationCount = 0;
295
+ const maxRevalidations = 2;
296
+
297
+ while (!integrity.valid && revalidationCount < maxRevalidations) {
298
+ console.log(`\n[FINALIZER WARNING] Workspace mutated during validation! Re-running validation to stabilize.`);
299
+ console.log(`Changes detected:`, integrity.changes);
300
+
301
+ checkBranchSafety();
302
+
303
+ // Update snapshot to current workspace state before validation runs
304
+ lastWriteSnapshot = await finalizer.snapshotManager.capture();
305
+ revalidationCount++;
306
+
307
+ // Re-run validation
308
+ result = await delegationController.validate(taskSlug, plan.mode, plan.profile, validateWorkflow);
309
+
310
+ checkBranchSafety();
311
+
312
+ // Verify integrity against the snapshot taken before this validation iteration
313
+ integrity = await finalizer.verifyIntegrity(lastWriteSnapshot);
314
+ }
315
+
316
+ let finalStatus = result.overallStatus;
317
+ if (!integrity.valid) {
318
+ console.log(`\n[FINALIZER BLOCKED] BLOCKED: workspace did not stabilize`);
319
+ console.log(`Final changes detected:`, integrity.changes);
320
+ finalStatus = "FAIL_QUALITY_GATE";
321
+ }
322
+
323
+ const finalState = finalStatus === "PASS"
324
+ ? "COMPLETED"
325
+ : finalStatus === "PASS_WITH_NOTES"
326
+ ? "COMPLETED_WITH_NOTES"
327
+ : "BLOCKED";
328
+
329
+ stateMachine.transitionTo(finalState);
330
+
331
+ console.log("\n--- Final Handoff ---");
332
+ const handoffEngine = new HandoffEngine({ cwd });
333
+ const handoffPath = await handoffEngine.generate({
334
+ taskId: taskSlug,
335
+ status: finalState,
336
+ specPaths: plan.specPath ? [plan.specPath] : [],
337
+ evidence: result.evidence,
338
+ nextActions: `Profile ${plan.profile}. Observed execution completed with status ${finalState}.`
141
339
  });
142
- }
143
340
 
144
- if (isTerminalFailure(result.overallStatus)) {
145
- stateMachine.transitionTo("BLOCKED");
146
- throw new Error(`[WORKFLOW BLOCKED] ${result.overallStatus}: Validation could not be resolved safely.`);
147
- }
341
+ console.log(`\n[AI WORKFLOW COMPLETE] ${finalState}`);
342
+ console.log(`Handoff Packet: ${path.relative(cwd, handoffPath)}\n`);
148
343
 
149
- // 8. Finalizer
150
- const finalState = result.overallStatus === "PASS"
151
- ? "COMPLETED"
152
- : result.overallStatus === "PASS_WITH_NOTES"
153
- ? "COMPLETED_WITH_NOTES"
154
- : "BLOCKED";
155
-
156
- stateMachine.transitionTo(finalState);
157
-
158
- console.log("\n--- Final Handoff ---");
159
- const handoffEngine = new HandoffEngine({ cwd });
160
- const handoffPath = await handoffEngine.generate({
161
- taskId: taskSlug,
162
- status: finalState,
163
- specPaths: plan.specPath ? [plan.specPath] : [],
164
- evidence: result.evidence,
165
- nextActions: `Profile ${plan.profile}. Observed execution completed with status ${finalState}.`
166
- });
167
-
168
- console.log(`\n[AI WORKFLOW COMPLETE] ${finalState}`);
169
- console.log(`Handoff Packet: ${path.relative(cwd, handoffPath)}\n`);
170
-
171
- return { ...result, stateHistory: stateMachine.getHistory() };
344
+ return { ...result, overallStatus: finalStatus, stateHistory: stateMachine.getHistory() };
345
+ } finally {
346
+ if (typeof plan !== "undefined" && plan && !plan.branchNeeded) {
347
+ console.log(`\n[READ-ONLY LEDGER] (In-Memory/Stdout)\n${JSON.stringify(ledger.getEvents(), null, 2)}\n`);
348
+ } else {
349
+ try {
350
+ await ledger.save(`.ai-workflow/history/${taskSlug}-ledger.json`);
351
+ } catch (err) {
352
+ // ignore or log
353
+ }
354
+ }
355
+ }
172
356
  }
@@ -0,0 +1,134 @@
1
+ import { OpenCodeAdapter } from "./runtime/opencode-adapter.js";
2
+
3
+ /**
4
+ * DelegationController - Manages and logs coordination between Atlas, Astra, Sage, and Phoenix.
5
+ */
6
+ export class DelegationController {
7
+ constructor({ cwd = process.cwd(), ledger, adapter } = {}) {
8
+ this.cwd = cwd;
9
+ this.ledger = ledger;
10
+ this.adapter = adapter || new OpenCodeAdapter({ cwd });
11
+ }
12
+
13
+ /**
14
+ * Logs routing decision by Atlas.
15
+ */
16
+ async route(naturalRequest, classification) {
17
+ if (this.ledger) {
18
+ this.ledger.logEvent({
19
+ actor: "Atlas",
20
+ actorType: "control-plane",
21
+ observed: true,
22
+ eventType: "routing",
23
+ provenance: "request-classifier",
24
+ data: {
25
+ request: naturalRequest,
26
+ classification,
27
+ event: "routing.completed"
28
+ }
29
+ });
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Logs and executes the implementation process by Astra.
35
+ */
36
+ async implement(promptMsg, owner, options = {}) {
37
+ if (this.ledger) {
38
+ this.ledger.logEvent({
39
+ actor: owner,
40
+ actorType: "runtime-agent",
41
+ observed: true,
42
+ runtime: "opencode",
43
+ eventType: "implementation_start",
44
+ provenance: "opencode-adapter",
45
+ data: {
46
+ agent: owner,
47
+ prompt: promptMsg,
48
+ event: "implementation.started"
49
+ }
50
+ });
51
+ }
52
+
53
+ const runResult = await this.adapter.execute(promptMsg, { agent: owner, ...options });
54
+
55
+ if (this.ledger) {
56
+ this.ledger.logEvent({
57
+ actor: owner,
58
+ actorType: "runtime-agent",
59
+ observed: true,
60
+ runtime: "opencode",
61
+ eventType: "implementation_complete",
62
+ provenance: "opencode-adapter",
63
+ data: {
64
+ success: runResult.success,
65
+ commandsRun: runResult.commandsRun || [],
66
+ eventCount: runResult.eventCount || 0,
67
+ error: runResult.error,
68
+ event: "implementation.completed"
69
+ }
70
+ });
71
+ }
72
+
73
+ return runResult;
74
+ }
75
+
76
+ /**
77
+ * Runs validation locally without logging Sage since no validation agent runtime was executed.
78
+ */
79
+ async validate(taskSlug, mode, profile, verifyFn) {
80
+ const result = await verifyFn();
81
+
82
+ if (mode === "full") {
83
+ if (this.ledger) {
84
+ this.ledger.logEvent({
85
+ actor: "Sage",
86
+ actorType: "auditor",
87
+ observed: true,
88
+ runtime: "opencode",
89
+ eventType: "validation_start",
90
+ provenance: "opencode-adapter",
91
+ data: {
92
+ agent: "Sage",
93
+ event: "validation.started"
94
+ }
95
+ });
96
+ }
97
+
98
+ let gitDiff = "";
99
+ try {
100
+ const { execSync } = await import("node:child_process");
101
+ gitDiff = execSync("git diff HEAD", { cwd: this.cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
102
+ } catch {
103
+ // ignore
104
+ }
105
+
106
+ const prompt = `Please validate the implementation. Diff:\n${gitDiff}\nLocal Evidence:\n${JSON.stringify(result, null, 2)}`;
107
+ const runResult = await this.adapter.execute(prompt, { agent: "Sage" });
108
+
109
+ if (this.ledger) {
110
+ this.ledger.logEvent({
111
+ actor: "Sage",
112
+ actorType: "auditor",
113
+ observed: true,
114
+ runtime: "opencode",
115
+ eventType: "validation_complete",
116
+ provenance: "opencode-adapter",
117
+ data: {
118
+ success: runResult.success,
119
+ commandsRun: runResult.commandsRun || [],
120
+ eventCount: runResult.eventCount || 0,
121
+ error: runResult.error,
122
+ event: "validation.completed"
123
+ }
124
+ });
125
+ }
126
+
127
+ if (!runResult.success) {
128
+ result.overallStatus = "FAIL_QUALITY_GATE";
129
+ }
130
+ }
131
+
132
+ return result;
133
+ }
134
+ }
@@ -0,0 +1,53 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ /**
5
+ * EvidenceLedger - Traces structural workflow events to make delegation observable.
6
+ */
7
+ export class EvidenceLedger {
8
+ constructor({ cwd = process.cwd(), workflowId = "unknown" } = {}) {
9
+ this.cwd = cwd;
10
+ this.workflowId = workflowId;
11
+ this.events = [];
12
+ }
13
+
14
+ /**
15
+ * Logs a workflow event to the ledger.
16
+ * @param {Object} eventParam
17
+ * @param {string} eventParam.actor - The agent name (Atlas, Astra, Sage, Phoenix).
18
+ * @param {string} eventParam.eventType - Type of event.
19
+ * @param {string} eventParam.provenance - Originating component.
20
+ * @param {Object} [eventParam.data] - Additional metadata.
21
+ * @returns {Object} The logged event.
22
+ */
23
+ logEvent({ actor, eventType, provenance, data = {}, actorType, observed, runtime }) {
24
+ const event = {
25
+ workflowId: this.workflowId,
26
+ timestamp: new Date().toISOString(),
27
+ actor,
28
+ actorType: actorType !== undefined ? actorType : undefined,
29
+ observed: observed !== undefined ? observed : undefined,
30
+ runtime: runtime !== undefined ? runtime : undefined,
31
+ eventType,
32
+ provenance,
33
+ data
34
+ };
35
+ this.events.push(event);
36
+ console.log(`[LEDGER] [${actor}] ${eventType}: ${JSON.stringify(data)}`);
37
+ return event;
38
+ }
39
+
40
+ getEvents() {
41
+ return this.events;
42
+ }
43
+
44
+ /**
45
+ * Saves the ledger events to a JSON file.
46
+ * @param {string} filePath - Absolute or relative path.
47
+ */
48
+ async save(filePath) {
49
+ const targetPath = path.isAbsolute(filePath) ? filePath : path.join(this.cwd, filePath);
50
+ await fs.mkdir(path.dirname(targetPath), { recursive: true });
51
+ await fs.writeFile(targetPath, JSON.stringify(this.events, null, 2), "utf8");
52
+ }
53
+ }
@@ -21,6 +21,10 @@ export class ExecutionPlanner {
21
21
  ? path.join("docs/workflows", taskSlug, "spec.md")
22
22
  : null;
23
23
 
24
+ const owner = classification.intent === "write"
25
+ ? "Astra"
26
+ : classification.owner;
27
+
24
28
  // Build default expected validations based on the resolved profile
25
29
  const validationsExpected = [];
26
30
  if (classification.validationNeeded) {
@@ -45,7 +49,7 @@ export class ExecutionPlanner {
45
49
  objective: classification.request,
46
50
  scope: classification.intent === "read-only" ? "Analysis and verification" : "Implementation of requested behavior",
47
51
  restrictions,
48
- owner: classification.owner,
52
+ owner,
49
53
  skills: [...classification.skills],
50
54
  branchNeeded,
51
55
  branchName: branchNeeded ? `feat/${taskSlug}` : null,