@dv.nghiem/flowdeck 0.4.12 → 0.5.1

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.
Files changed (104) hide show
  1. package/dist/agents/orchestrator.d.ts.map +1 -1
  2. package/dist/config/index.d.ts +1 -1
  3. package/dist/config/index.d.ts.map +1 -1
  4. package/dist/config/schema.d.ts +27 -1
  5. package/dist/config/schema.d.ts.map +1 -1
  6. package/dist/dashboard/lib/state-reader.d.ts +2 -1
  7. package/dist/dashboard/lib/state-reader.d.ts.map +1 -1
  8. package/dist/dashboard/server.mjs +29 -1
  9. package/dist/dashboard/types.d.ts +14 -0
  10. package/dist/dashboard/types.d.ts.map +1 -1
  11. package/dist/dashboard/views/index.ejs +2 -0
  12. package/dist/dashboard/views/partials/token-budget.ejs +35 -0
  13. package/dist/hooks/approval-hook.d.ts +16 -2
  14. package/dist/hooks/approval-hook.d.ts.map +1 -1
  15. package/dist/hooks/compaction-hook.d.ts +1 -1
  16. package/dist/hooks/compaction-hook.d.ts.map +1 -1
  17. package/dist/hooks/context-window-monitor.d.ts +7 -1
  18. package/dist/hooks/context-window-monitor.d.ts.map +1 -1
  19. package/dist/hooks/decision-trace-hook.d.ts +3 -0
  20. package/dist/hooks/decision-trace-hook.d.ts.map +1 -1
  21. package/dist/hooks/event-log-hook.d.ts +19 -3
  22. package/dist/hooks/event-log-hook.d.ts.map +1 -1
  23. package/dist/hooks/guard-rails.d.ts +16 -5
  24. package/dist/hooks/guard-rails.d.ts.map +1 -1
  25. package/dist/hooks/orchestrator-guard-hook.d.ts +8 -5
  26. package/dist/hooks/orchestrator-guard-hook.d.ts.map +1 -1
  27. package/dist/hooks/tool-guard.d.ts +19 -3
  28. package/dist/hooks/tool-guard.d.ts.map +1 -1
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +9978 -5880
  31. package/dist/lib/lazy.d.ts +12 -0
  32. package/dist/lib/lazy.d.ts.map +1 -0
  33. package/dist/services/agent-contract-registry.d.ts.map +1 -1
  34. package/dist/services/agent-trace-graph.d.ts +4 -0
  35. package/dist/services/agent-trace-graph.d.ts.map +1 -1
  36. package/dist/services/agent-validator.d.ts +2 -1
  37. package/dist/services/agent-validator.d.ts.map +1 -1
  38. package/dist/services/approval-manager.d.ts +14 -1
  39. package/dist/services/approval-manager.d.ts.map +1 -1
  40. package/dist/services/audit-log.d.ts +23 -0
  41. package/dist/services/audit-log.d.ts.map +1 -0
  42. package/dist/services/context-ingress.d.ts +81 -0
  43. package/dist/services/context-ingress.d.ts.map +1 -0
  44. package/dist/services/council-runner.d.ts +19 -0
  45. package/dist/services/council-runner.d.ts.map +1 -0
  46. package/dist/services/deadlock-detector.d.ts.map +1 -1
  47. package/dist/services/delegation-budget.d.ts +55 -0
  48. package/dist/services/delegation-budget.d.ts.map +1 -0
  49. package/dist/services/event-handlers/command-executed-handler.d.ts +9 -0
  50. package/dist/services/event-handlers/command-executed-handler.d.ts.map +1 -0
  51. package/dist/services/event-handlers/index.d.ts +6 -0
  52. package/dist/services/event-handlers/index.d.ts.map +1 -0
  53. package/dist/services/event-handlers/session-error-handler.d.ts +8 -0
  54. package/dist/services/event-handlers/session-error-handler.d.ts.map +1 -0
  55. package/dist/services/event-handlers/session-idle-handler.d.ts +9 -0
  56. package/dist/services/event-handlers/session-idle-handler.d.ts.map +1 -0
  57. package/dist/services/event-handlers/tool-outcome-handler.d.ts +11 -0
  58. package/dist/services/event-handlers/tool-outcome-handler.d.ts.map +1 -0
  59. package/dist/services/event-handlers/types.d.ts +42 -0
  60. package/dist/services/event-handlers/types.d.ts.map +1 -0
  61. package/dist/services/event-logger.d.ts +3 -1
  62. package/dist/services/event-logger.d.ts.map +1 -1
  63. package/dist/services/execution-substrate.d.ts +35 -0
  64. package/dist/services/execution-substrate.d.ts.map +1 -0
  65. package/dist/services/harness-controller.d.ts +58 -0
  66. package/dist/services/harness-controller.d.ts.map +1 -0
  67. package/dist/services/harness-policy.d.ts +24 -0
  68. package/dist/services/harness-policy.d.ts.map +1 -0
  69. package/dist/services/harness-types.d.ts +185 -0
  70. package/dist/services/harness-types.d.ts.map +1 -0
  71. package/dist/services/lazy-rule-loader.d.ts +2 -0
  72. package/dist/services/lazy-rule-loader.d.ts.map +1 -1
  73. package/dist/services/prompt-cache.d.ts +25 -0
  74. package/dist/services/prompt-cache.d.ts.map +1 -0
  75. package/dist/services/recovery-layer.d.ts +26 -0
  76. package/dist/services/recovery-layer.d.ts.map +1 -0
  77. package/dist/services/run-trace.d.ts +17 -0
  78. package/dist/services/run-trace.d.ts.map +1 -1
  79. package/dist/services/state-persistence.d.ts +22 -0
  80. package/dist/services/state-persistence.d.ts.map +1 -0
  81. package/dist/services/supervisor-binding.d.ts +9 -0
  82. package/dist/services/supervisor-binding.d.ts.map +1 -1
  83. package/dist/services/token-metrics.d.ts +54 -0
  84. package/dist/services/token-metrics.d.ts.map +1 -0
  85. package/dist/services/verification-layer.d.ts +24 -0
  86. package/dist/services/verification-layer.d.ts.map +1 -0
  87. package/dist/services/workflow-scorecard.d.ts +5 -0
  88. package/dist/services/workflow-scorecard.d.ts.map +1 -1
  89. package/dist/tools/council.d.ts.map +1 -1
  90. package/dist/tools/decision-trace.d.ts +4 -0
  91. package/dist/tools/decision-trace.d.ts.map +1 -1
  92. package/dist/tools/delegate.d.ts +16 -0
  93. package/dist/tools/delegate.d.ts.map +1 -0
  94. package/dist/tools/failure-replay.d.ts +8 -0
  95. package/dist/tools/failure-replay.d.ts.map +1 -1
  96. package/dist/tools/policy-engine.d.ts +1 -0
  97. package/dist/tools/policy-engine.d.ts.map +1 -1
  98. package/dist/types/hooks.d.ts +54 -0
  99. package/dist/types/hooks.d.ts.map +1 -0
  100. package/docs/concepts/HARNESS_ARCHITECTURE.md +241 -0
  101. package/docs/concepts/HARNESS_LAYERS.md +378 -0
  102. package/docs/concepts/HARNESS_WIRING.md +404 -0
  103. package/package.json +1 -1
  104. package/src/commands/fd-guarded-edit.md +69 -0
@@ -0,0 +1,404 @@
1
+ # FlowDeck Harness Wiring
2
+
3
+ This document describes how the existing unwired services are wired into `src/index.ts` and the hook system to realize the target harness.
4
+
5
+ ## 1. Guiding rule
6
+
7
+ **Existing behavior stays opt-in.** The first wiring pass makes all new runtime checks advisory or feature-flagged. Strict enforcement is toggled via `flowdeck.json`.
8
+
9
+ ## 2. `src/index.ts` structure after wiring
10
+
11
+ The plugin factory becomes a thin lifecycle assembler:
12
+
13
+ ```typescript
14
+ const plugin: Plugin = async (input, _options) => {
15
+ const { directory, client, worktree } = input;
16
+ const appLog = /* existing */;
17
+
18
+ // ── 1. Core harness services (existing + new) ────────────────────────────
19
+ const contextIngress = createContextIngressService({ directory, client });
20
+ const actionMediator = createActionMediatorService({ directory });
21
+ const executionSubstrate = createExecutionSubstrateService({ directory, appLog });
22
+ const statePersistence = createStatePersistenceService({ directory });
23
+ const verification = createVerificationService({ directory });
24
+ const recovery = createRecoveryService({ directory });
25
+ const governance = createGovernanceService({ directory });
26
+ const coordination = createCoordinationService({ directory });
27
+
28
+ // ── 2. Existing wired services we keep ───────────────────────────────────
29
+ const fileTracker = new SessionFileTracker();
30
+ const { fileEdited, fileWatcherUpdated } = createFileTrackerHooks(fileTracker);
31
+ const contextMonitor = createContextWindowMonitorHook();
32
+ const shellEnvHook = createShellEnvHook({ directory, worktree });
33
+ const todoHook = createTodoHook(client);
34
+ const sessionIdleHook = createSessionIdleHook(client, fileTracker);
35
+ const compactionHook = createCompactionHook({ directory }, fileTracker);
36
+ const orchestratorGuard = new OrchestratorGuard();
37
+ const autoLearnHook = createAutoLearnHook(client, fileTracker, directory, appLog);
38
+ const notifCtrl = new NotificationController(undefined, appLog);
39
+
40
+ // ── 3. Services previously unwired, now instantiated ─────────────────────
41
+ const agentContracts = getAllContracts(); // agent-contract-registry
42
+ const delegationBudget = createDelegationBudgetService();
43
+ const quickRouter = createQuickRouter(directory); // quick-router + workflow-router
44
+
45
+ let loopDetector: LoopDetector | undefined;
46
+ let eventLog: ReturnType<typeof createEventLogHooks> | undefined;
47
+ let lastExecutedCommand: string | null = null;
48
+ let activeRun: RunTrace | undefined;
49
+
50
+ return {
51
+ name: "@dv.nghiem/flowdeck",
52
+ agent: getAgentConfigs(agentModels),
53
+ mcp: createFlowDeckMcps(),
54
+
55
+ config: async (cfg) => {
56
+ // existing config logic: default_agent, agent configs, MCPs, commands, skills, rules
57
+ // plus new wiring below
58
+ const flowdeckConfig = loadFlowDeckConfig(directory);
59
+ const loopCfg = flowdeckConfig.governance?.loopDetection ?? {};
60
+ loopDetector = new LoopDetector({ ... }, appLog);
61
+
62
+ eventLog = createEventLogHooks(appLog, (toolName, args, output, sessionId, status) => {
63
+ loopDetector?.recordAfter(toolName, args, output, sessionId, status);
64
+ executionSubstrate?.recordToolEvent(toolName, sessionId);
65
+ });
66
+ },
67
+
68
+ tool: {
69
+ // existing tools
70
+ "planning-state": planningStateTool,
71
+ "codebase-state": codebaseStateTool,
72
+ "repo-memory": repoMemoryTool,
73
+ "failure-replay": failureReplayTool,
74
+ "decision-trace": decisionTraceTool,
75
+ "policy-engine": policyEngineTool,
76
+ "hash-edit": hashEditTool,
77
+ "council": councilTool,
78
+ "reflect": reflectTool,
79
+ "codegraph": codegraphTool,
80
+ "load-rules": loadRulesTool,
81
+ "list-rules": listRulesTool,
82
+ "merge-assist": mergeAssistTool,
83
+
84
+ // NEW: harness dispatchers
85
+ "delegate": createDelegateTool({
86
+ directory,
87
+ governance,
88
+ actionMediator,
89
+ executionSubstrate,
90
+ coordination,
91
+ delegationBudget,
92
+ }),
93
+ "run-pipeline": createRunPipelineTool({
94
+ directory,
95
+ contextIngress,
96
+ coordination,
97
+ executionSubstrate,
98
+ statePersistence,
99
+ verification,
100
+ recovery,
101
+ }),
102
+ },
103
+
104
+ // existing hooks
105
+ "shell.env": shellEnvHook,
106
+ "todo.updated": todoHook,
107
+ "file.edited": fileEdited,
108
+ "file.watcher.updated": fileWatcherUpdated,
109
+ "experimental.session.compacting": compactionHook,
110
+
111
+ "command.execute.before": async (input) => {
112
+ lastExecutedCommand = input.command;
113
+ activeRun = executionSubstrate.startRun(
114
+ input.command,
115
+ input.arguments ? JSON.parse(input.arguments) : {},
116
+ input.sessionID,
117
+ );
118
+ },
119
+
120
+ "permission.ask": async (input, output) => {
121
+ notifyPermissionNeeded(input.title);
122
+ // optionally: run actionMediator to pre-classify risk before the UI asks
123
+ },
124
+
125
+ event: async ({ event }) => {
126
+ const type = event?.type ?? "";
127
+
128
+ if (type === "session.created" || type === "session.started") {
129
+ await sessionStartHook({ directory });
130
+ if (type === "session.created") {
131
+ await eventLog!.session({ directory }, event);
132
+ }
133
+ }
134
+
135
+ if (type === "command.executed") {
136
+ const commandName = event?.properties?.name ?? "";
137
+ if (commandName) notifCtrl.onCommandExecuted(commandName);
138
+ }
139
+
140
+ await contextMonitor.event({ event });
141
+ orchestratorGuard.onEvent(event);
142
+
143
+ if (type === "session.idle") {
144
+ await eventLog!.session({ directory }, event);
145
+ const hasEdits = fileTracker.getEditedPaths().length > 0;
146
+ if (lastExecutedCommand) lastExecutedCommand = null;
147
+ notifCtrl.onSessionIdle(hasEdits);
148
+
149
+ if (activeRun) {
150
+ executionSubstrate.endRun(activeRun.run_id, "complete");
151
+ verification.verifyStage("idle", activeRun.run_id);
152
+ activeRun = undefined;
153
+ }
154
+
155
+ try {
156
+ await sessionIdleHook();
157
+ await autoLearnHook();
158
+ } finally {
159
+ fileTracker.clear();
160
+ }
161
+ }
162
+
163
+ if (type === "session.error") {
164
+ await eventLog!.session({ directory }, event);
165
+ lastExecutedCommand = null;
166
+ const errorMsg = /* existing extraction */;
167
+ notifCtrl.onSessionError(errorMsg);
168
+ if (activeRun) {
169
+ executionSubstrate.endRun(activeRun.run_id, "failed", errorMsg);
170
+ recovery.assessFailure(activeRun.run_id, event?.properties?.error);
171
+ activeRun = undefined;
172
+ }
173
+ }
174
+ },
175
+
176
+ "tool.execute.before": async (toolInput, toolOutput) => {
177
+ // existing arg normalization
178
+ if ((toolInput.tool === "read" || toolInput.tool === "view") && toolOutput?.args) {
179
+ // ... existing offset normalization
180
+ }
181
+
182
+ orchestratorGuard.check(toolInput.sessionID ?? "", toolInput.tool ?? toolInput.name ?? "");
183
+
184
+ const runId = activeRun?.run_id ?? "no-run";
185
+ const decision = actionMediator.check({
186
+ toolName: toolInput.tool ?? toolInput.name ?? "unknown",
187
+ args: toolOutput?.args ?? toolInput?.args ?? {},
188
+ agentName: getCurrentAgent() ?? undefined,
189
+ runId,
190
+ sessionId: toolInput.sessionID ?? "",
191
+ });
192
+
193
+ if (decision.action === "block") {
194
+ throw new Error(decision.reason);
195
+ }
196
+ if (decision.action === "ask" && decision.requiredApprovalId) {
197
+ // OpenCode permission.ask is already in flight; we record the pending approval
198
+ approvalManager.requestApproval(directory, runId, toolInput.tool, decision.reason, {
199
+ session_id: toolInput.sessionID,
200
+ risk_score: decision.riskScore,
201
+ });
202
+ }
203
+
204
+ // legacy hooks kept for compatibility
205
+ await approvalHook({ directory }, toolInput, toolOutput);
206
+ await guardRailsHook({ directory }, toolInput, toolOutput);
207
+ await toolGuardHook({ directory }, toolInput, toolOutput);
208
+ await patchTrustHook({ directory }, toolInput, toolOutput);
209
+ await decisionTraceHook({ directory }, toolInput, toolOutput);
210
+ await eventLog!.before({ directory }, toolInput, toolOutput);
211
+
212
+ const loopResult = loopDetector!.checkBefore(
213
+ toolInput.tool ?? toolInput.name ?? "unknown",
214
+ toolOutput?.args ?? toolInput?.args ?? {},
215
+ toolInput.sessionID ?? "",
216
+ );
217
+ if (loopResult.action === "block") {
218
+ throw new Error(loopResult.escalationMessage);
219
+ }
220
+ if (loopResult.action === "warn") {
221
+ appLog(loopResult.message);
222
+ }
223
+ },
224
+
225
+ "tool.execute.after": async (toolInput, toolOutput) => {
226
+ const eventLogHealthy = await eventLog!.after({ directory }, toolInput, toolOutput);
227
+ if (!eventLogHealthy) {
228
+ loopDetector!.setPersistenceHealthy(false);
229
+ }
230
+ await contextMonitor["tool.execute.after"](toolInput, toolOutput);
231
+
232
+ actionMediator.recordOutcome(
233
+ {
234
+ toolName: toolInput.tool ?? toolInput.name ?? "unknown",
235
+ args: toolOutput?.args ?? toolInput?.args ?? {},
236
+ agentName: getCurrentAgent() ?? undefined,
237
+ runId: activeRun?.run_id ?? "no-run",
238
+ sessionId: toolInput.sessionID ?? "",
239
+ },
240
+ { action: "allow", reason: "executed", riskScore: 0 },
241
+ toolOutput,
242
+ );
243
+ },
244
+ };
245
+ };
246
+ ```
247
+
248
+ ## 3. New tools
249
+
250
+ ### 3.1 `delegate` tool
251
+
252
+ Located at `src/tools/delegate.ts`.
253
+
254
+ **Purpose**: Imperative agent/command dispatch from the orchestrator.
255
+
256
+ **Inputs/outputs**: see `HARNESS_ARCHITECTURE.md` §5.3.
257
+
258
+ **Behavior**:
259
+
260
+ 1. Resolve target via `supervisor-binding` (`isRegisteredCommand` / `isRegisteredAgent`).
261
+ 2. Load the agent contract from `agent-contract-registry`.
262
+ 3. Run `agent-validator` against the requested target and task type.
263
+ 4. Run `supervisor-binding.runSupervisorReview` if supervisor is enabled.
264
+ 5. Check `delegation-budget` (depth, tool-call count, same-step retries).
265
+ 6. Open an `AgentSpan` in `agent-trace-graph` linked to the parent span.
266
+ 7. Return `DelegateResult` with `spanId` and child session info.
267
+ 8. The actual child agent invocation still uses OpenCode native `@agent` routing; the tool records and governs it.
268
+
269
+ ### 3.2 `run-pipeline` tool
270
+
271
+ Located at `src/tools/run-pipeline.ts`.
272
+
273
+ **Purpose**: Drive a multi-stage workflow (discuss → plan → execute → verify) without relying on the orchestrator to remember state.
274
+
275
+ **Behavior**:
276
+
277
+ 1. Classify task with `quick-router` + `workflow-router`.
278
+ 2. Load or create `RunState` via `state-persistence`.
279
+ 3. For each pending stage:
280
+ - Call `delegate` for the appropriate command/agent.
281
+ - Wait for `session.idle` or `session.error`.
282
+ - Call `verification.verifyStage`.
283
+ - If blocked, record `blocked=true` and reason, then stop.
284
+ 4. Update `.planning/STATE.md` via `planning-state` after each completed stage.
285
+ 5. On completion, call `workflow-scorecard.generateScorecard`.
286
+
287
+ ### 3.3 `delegation-budget` service
288
+
289
+ Located at `src/services/delegation-budget.ts`.
290
+
291
+ **Purpose**: Enforce per-run limits that README already advertises but that currently have no runtime implementation.
292
+
293
+ **Wiring**:
294
+
295
+ - Initialized when `activeRun` starts.
296
+ - Checked inside `delegate` tool.
297
+ - Checked inside `tool.execute.before` for every tool call that belongs to a run.
298
+ - Config read from `flowdeckConfig.governance.delegationBudget` (README mentions `maxToolCalls`, `maxDepth`, `maxSameStepRetries`).
299
+
300
+ ## 4. Hook wiring changes
301
+
302
+ | Hook | Current | After wiring |
303
+ |------|---------|--------------|
304
+ | `command.execute.before` | Records `lastExecutedCommand` | Also starts a `RunTrace` and initializes the delegation budget |
305
+ | `command.execute.after` | Not used | Ends the run trace and triggers scorecard generation |
306
+ | `tool.execute.before` | Runs approval, guard-rails, tool-guard, patch-trust, decision-trace, event-log, loop-detector sequentially | Routes all checks through `ActionMediator`; keeps legacy hooks for compatibility |
307
+ | `tool.execute.after` | Event-log + context monitor | Also records action outcome and updates spans/cost |
308
+ | `event` (session.idle) | Notifications + auto-learn | Also ends run, runs verification, scorecard |
309
+ | `event` (session.error) | Notifications | Also ends run as failed, runs recovery assessment |
310
+ | `permission.ask` | Notification only | Optionally records pending approval in `approval-manager` |
311
+
312
+ ## 5. Existing unwired services: wiring map
313
+
314
+ | Service | New wiring location | What it does at runtime |
315
+ |---------|---------------------|-------------------------|
316
+ | `agent-contract-registry` | `ActionMediator`, `GovernanceService`, `delegate` tool | Validates tool/task access per agent |
317
+ | `agent-validator` | `ActionMediator`, `GovernanceService` | Emits allow/warn/block/escalate for agent invocations |
318
+ | `agent-trace-graph` | `ExecutionSubstrate`, `delegate` tool | Records causal parent-child agent spans |
319
+ | `run-trace` | `ExecutionSubstrate`, `command.execute.before/after` | Tracks command-level runs |
320
+ | `workflow-scorecard` | `event` (session.idle) | Generates scorecard on run completion |
321
+ | `deadlock-detector` | `RecoveryService`, scheduled check on `session.idle` | Detects bounce/circular/retry/stall signals |
322
+ | `model-router` | `ContextIngressService`, `CoordinationService` | Classifies complexity and slims orchestrator prompt |
323
+ | `workflow-router` | `CoordinationService`, `run-pipeline` tool | Selects workflow class and stage sequence |
324
+ | `quick-router` | `run-pipeline` tool, orchestrator prompt | Classifies task and builds stage sequence |
325
+ | `preflight-explorer` | `ContextIngressService` | Provides repo evidence to avoid unnecessary questions |
326
+ | `cost-estimator` | `ExecutionSubstrate` | Estimates USD cost per tool/agent call |
327
+ | `approval-manager` | `ActionMediator`, `approval-hook`, `permission.ask` | Stores and checks approvals |
328
+ | `supervisor-binding` | `ActionMediator`, `GovernanceService`, `delegate` tool | Structured preflight/post-stage review |
329
+ | `command-validator` | `GovernanceService`, `command-ref-guard` hook | Blocks unregistered command references |
330
+ | `question-guard` | `ContextIngressService` | Suppresses redundant questions |
331
+ | `agent-performance` | `ExecutionSubstrate`, `RecoveryService` | Tracks success rates and recommends re-routing |
332
+
333
+ ## 6. Service instantiation lifecycle
334
+
335
+ ```
336
+ Plugin factory
337
+
338
+ ├── config() → create LoopDetector, EventLog hooks, load flowdeck.json
339
+
340
+ ├── command.execute.before
341
+ │ → start RunTrace
342
+ │ → init DelegationBudget
343
+
344
+ ├── tool.execute.before
345
+ │ → ActionMediator.check() (contracts, validator, supervisor, approvals, loop)
346
+ │ → legacy hooks (opt-in)
347
+
348
+ ├── tool.execute.after
349
+ │ → EventLog.after()
350
+ │ → ActionMediator.recordOutcome()
351
+
352
+ ├── delegate tool → Governance review + budget check + open AgentSpan
353
+
354
+ ├── run-pipeline tool → Coordination + StatePersistence + Verification
355
+
356
+ ├── session.idle → end RunTrace, verify, scorecard, auto-learn
357
+
358
+ └── session.error → end RunTrace as failed, recovery assessment
359
+ ```
360
+
361
+ ## 7. Configuration flags
362
+
363
+ All new runtime behavior is controlled through the existing `flowdeck.json` schema (`src/config/schema.ts`):
364
+
365
+ ```json
366
+ {
367
+ "governance": {
368
+ "validator": { "mode": "advisory" },
369
+ "delegationBudget": { "maxToolCalls": 200, "maxDepth": 8, "maxSameStepRetries": 3 },
370
+ "deadlockDetection": { "enabled": true, "bounceThreshold": 3, "autoStop": false },
371
+ "scorecard": { "enabled": true },
372
+ "supervisor": { "enabled": false, "mode": "advisory" },
373
+ "costBudget": { "maxEstimatedCostUSD": 5.0, "onExhaustion": "warn" }
374
+ }
375
+ }
376
+ ```
377
+
378
+ New environment flags:
379
+
380
+ | Flag | Purpose |
381
+ |------|---------|
382
+ | `FLOWDECK_DELEGATE_ENABLED=1` | Enable `delegate` tool |
383
+ | `FLOWDECK_RUN_PIPELINE_ENABLED=1` | Enable `run-pipeline` tool |
384
+ | `FLOWDECK_ACTION_MEDIATOR_STRICT=1` | Treat `ActionMediator` `block` as fatal even in advisory validator mode |
385
+
386
+ ## 8. Verification checklist for the wiring PR
387
+
388
+ - [ ] `src/index.ts` compiles and existing tests pass.
389
+ - [ ] `agent-validator`, `agent-trace-graph`, `run-trace`, `workflow-scorecard`, `deadlock-detector` are imported and instantiated.
390
+ - [ ] `delegate` and `run-pipeline` tools are registered.
391
+ - [ ] `ActionMediator` is called in `tool.execute.before` and `.after`.
392
+ - [ ] `RunTrace` is started in `command.execute.before` and ended in `session.idle`/`session.error`.
393
+ - [ ] `WorkflowScorecard` is generated on run completion.
394
+ - [ ] No new hardcoded secrets or credentials.
395
+ - [ ] New services have unit tests before strict mode is enabled.
396
+
397
+ ## 9. Open questions
398
+
399
+ 1. Should `delegate` open the child session itself, or only record after OpenCode routes it?
400
+ **Recommendation**: Only record; OpenCode owns session creation. The tool returns a `spanId` immediately and the `event` hook links the child session via `parentID`.
401
+ 2. Should `run-pipeline` run stages synchronously inside one tool call, or return after each stage and rely on resume?
402
+ **Recommendation**: Return after each stage and store `RunState`; resume via `/fd-resume` or the next `run-pipeline` call. This avoids long-running tool timeouts.
403
+ 3. Where should delegation-budget state live?
404
+ **Recommendation**: In-memory per run, persisted into `RUNS.jsonl` fields on run end. No separate mutable file needed in the first pass.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dv.nghiem/flowdeck",
3
- "version": "0.4.12",
3
+ "version": "0.5.1",
4
4
  "description": "FlowDeck — structured planning and execution workflows for OpenCode",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -0,0 +1,69 @@
1
+ ---
2
+ description: Review and approve a sensitive-file edit through the FlowDeck approval manager
3
+ argument-hint: --file PATH [--reason TEXT]
4
+ ---
5
+
6
+ # Guarded Edit
7
+
8
+ Request or confirm human approval for writing/editing a sensitive file (auth, payment, secrets, infra, migrations, etc.). This command is the canonical way to satisfy an `APPROVAL_REQUIRED` block from the approval hook.
9
+
10
+ **Input:** `$ARGUMENTS` — required `--file PATH`; optional `--reason TEXT`
11
+
12
+ ## Pre-flight
13
+
14
+ 1. Check `.codebase/APPROVALS.json` for any pending request matching the file path.
15
+ 2. If no pending request exists, create one with the current run/session context.
16
+
17
+ ## Process
18
+
19
+ ### Step 1: Present the request
20
+
21
+ Show the user:
22
+
23
+ ```
24
+ ════════════════════════════════════════════════════
25
+ APPROVAL REQUIRED: <file_path>
26
+ ════════════════════════════════════════════════════
27
+
28
+ Agent: <agent_name>
29
+ Run: <run_id>
30
+ Session: <session_id>
31
+ Reason: <reason or "Sensitive path detected">
32
+
33
+ Change description: <tool and target>
34
+
35
+ [ ] I have reviewed the change and approve it
36
+ [ ] Reject — do not proceed
37
+ ```
38
+
39
+ ### Step 2: Resolve via the policy engine
40
+
41
+ Use the `policy-engine` tool to record the decision:
42
+
43
+ - **Approve:**
44
+ ```
45
+ policy-engine action=resolve policy_id=<approval_id> decision=approved
46
+ ```
47
+
48
+ - **Reject:**
49
+ ```
50
+ policy-engine action=resolve policy_id=<approval_id> decision=rejected
51
+ ```
52
+
53
+ The approval ID is the `id` field of the request in `.codebase/APPROVALS.json`.
54
+
55
+ ## Constraints
56
+
57
+ - Approval is bound to `(run_id, session_id, agent, file_path, content_hash)`. Re-approval is required if any of these change.
58
+ - Approved requests expire after 30 minutes.
59
+ - Only approve edits you have actually reviewed.
60
+
61
+ ## Error Handling
62
+
63
+ - If `--file` is missing: error "Usage: /fd-guarded-edit --file PATH [--reason TEXT]"
64
+ - If no pending request exists and one cannot be created: error "Could not create approval request. Ensure an active run context exists."
65
+ - If the file path is not sensitive: warn "This path does not require explicit approval."
66
+
67
+ ## Completion
68
+
69
+ Report the resolution (approved/rejected) and the approval ID. If approved, the original tool call can be retried.