@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.
- package/dist/agents/orchestrator.d.ts.map +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/schema.d.ts +27 -1
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/dashboard/lib/state-reader.d.ts +2 -1
- package/dist/dashboard/lib/state-reader.d.ts.map +1 -1
- package/dist/dashboard/server.mjs +29 -1
- package/dist/dashboard/types.d.ts +14 -0
- package/dist/dashboard/types.d.ts.map +1 -1
- package/dist/dashboard/views/index.ejs +2 -0
- package/dist/dashboard/views/partials/token-budget.ejs +35 -0
- package/dist/hooks/approval-hook.d.ts +16 -2
- package/dist/hooks/approval-hook.d.ts.map +1 -1
- package/dist/hooks/compaction-hook.d.ts +1 -1
- package/dist/hooks/compaction-hook.d.ts.map +1 -1
- package/dist/hooks/context-window-monitor.d.ts +7 -1
- package/dist/hooks/context-window-monitor.d.ts.map +1 -1
- package/dist/hooks/decision-trace-hook.d.ts +3 -0
- package/dist/hooks/decision-trace-hook.d.ts.map +1 -1
- package/dist/hooks/event-log-hook.d.ts +19 -3
- package/dist/hooks/event-log-hook.d.ts.map +1 -1
- package/dist/hooks/guard-rails.d.ts +16 -5
- package/dist/hooks/guard-rails.d.ts.map +1 -1
- package/dist/hooks/orchestrator-guard-hook.d.ts +8 -5
- package/dist/hooks/orchestrator-guard-hook.d.ts.map +1 -1
- package/dist/hooks/tool-guard.d.ts +19 -3
- package/dist/hooks/tool-guard.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9978 -5880
- package/dist/lib/lazy.d.ts +12 -0
- package/dist/lib/lazy.d.ts.map +1 -0
- package/dist/services/agent-contract-registry.d.ts.map +1 -1
- package/dist/services/agent-trace-graph.d.ts +4 -0
- package/dist/services/agent-trace-graph.d.ts.map +1 -1
- package/dist/services/agent-validator.d.ts +2 -1
- package/dist/services/agent-validator.d.ts.map +1 -1
- package/dist/services/approval-manager.d.ts +14 -1
- package/dist/services/approval-manager.d.ts.map +1 -1
- package/dist/services/audit-log.d.ts +23 -0
- package/dist/services/audit-log.d.ts.map +1 -0
- package/dist/services/context-ingress.d.ts +81 -0
- package/dist/services/context-ingress.d.ts.map +1 -0
- package/dist/services/council-runner.d.ts +19 -0
- package/dist/services/council-runner.d.ts.map +1 -0
- package/dist/services/deadlock-detector.d.ts.map +1 -1
- package/dist/services/delegation-budget.d.ts +55 -0
- package/dist/services/delegation-budget.d.ts.map +1 -0
- package/dist/services/event-handlers/command-executed-handler.d.ts +9 -0
- package/dist/services/event-handlers/command-executed-handler.d.ts.map +1 -0
- package/dist/services/event-handlers/index.d.ts +6 -0
- package/dist/services/event-handlers/index.d.ts.map +1 -0
- package/dist/services/event-handlers/session-error-handler.d.ts +8 -0
- package/dist/services/event-handlers/session-error-handler.d.ts.map +1 -0
- package/dist/services/event-handlers/session-idle-handler.d.ts +9 -0
- package/dist/services/event-handlers/session-idle-handler.d.ts.map +1 -0
- package/dist/services/event-handlers/tool-outcome-handler.d.ts +11 -0
- package/dist/services/event-handlers/tool-outcome-handler.d.ts.map +1 -0
- package/dist/services/event-handlers/types.d.ts +42 -0
- package/dist/services/event-handlers/types.d.ts.map +1 -0
- package/dist/services/event-logger.d.ts +3 -1
- package/dist/services/event-logger.d.ts.map +1 -1
- package/dist/services/execution-substrate.d.ts +35 -0
- package/dist/services/execution-substrate.d.ts.map +1 -0
- package/dist/services/harness-controller.d.ts +58 -0
- package/dist/services/harness-controller.d.ts.map +1 -0
- package/dist/services/harness-policy.d.ts +24 -0
- package/dist/services/harness-policy.d.ts.map +1 -0
- package/dist/services/harness-types.d.ts +185 -0
- package/dist/services/harness-types.d.ts.map +1 -0
- package/dist/services/lazy-rule-loader.d.ts +2 -0
- package/dist/services/lazy-rule-loader.d.ts.map +1 -1
- package/dist/services/prompt-cache.d.ts +25 -0
- package/dist/services/prompt-cache.d.ts.map +1 -0
- package/dist/services/recovery-layer.d.ts +26 -0
- package/dist/services/recovery-layer.d.ts.map +1 -0
- package/dist/services/run-trace.d.ts +17 -0
- package/dist/services/run-trace.d.ts.map +1 -1
- package/dist/services/state-persistence.d.ts +22 -0
- package/dist/services/state-persistence.d.ts.map +1 -0
- package/dist/services/supervisor-binding.d.ts +9 -0
- package/dist/services/supervisor-binding.d.ts.map +1 -1
- package/dist/services/token-metrics.d.ts +54 -0
- package/dist/services/token-metrics.d.ts.map +1 -0
- package/dist/services/verification-layer.d.ts +24 -0
- package/dist/services/verification-layer.d.ts.map +1 -0
- package/dist/services/workflow-scorecard.d.ts +5 -0
- package/dist/services/workflow-scorecard.d.ts.map +1 -1
- package/dist/tools/council.d.ts.map +1 -1
- package/dist/tools/decision-trace.d.ts +4 -0
- package/dist/tools/decision-trace.d.ts.map +1 -1
- package/dist/tools/delegate.d.ts +16 -0
- package/dist/tools/delegate.d.ts.map +1 -0
- package/dist/tools/failure-replay.d.ts +8 -0
- package/dist/tools/failure-replay.d.ts.map +1 -1
- package/dist/tools/policy-engine.d.ts +1 -0
- package/dist/tools/policy-engine.d.ts.map +1 -1
- package/dist/types/hooks.d.ts +54 -0
- package/dist/types/hooks.d.ts.map +1 -0
- package/docs/concepts/HARNESS_ARCHITECTURE.md +241 -0
- package/docs/concepts/HARNESS_LAYERS.md +378 -0
- package/docs/concepts/HARNESS_WIRING.md +404 -0
- package/package.json +1 -1
- 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
|
@@ -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.
|