@dv.nghiem/flowdeck 0.5.2 → 0.5.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/dist/index.js CHANGED
@@ -1259,7 +1259,7 @@ Before routing, you MUST emit a routing decision in this exact format:
1259
1259
  \`\`\`
1260
1260
 
1261
1261
  ### Step 5: Route and Supervise
1262
- - Use the \`delegate\` tool to assign work to the selected agent(s)
1262
+ - Use native OpenCode agent routing: mention the agent with \`@agent-name <full task description>\`
1263
1263
  - Provide clear, focused context including task description and any relevant files
1264
1264
  - Wait for completion
1265
1265
  - Collect results
@@ -1276,7 +1276,6 @@ You may ONLY use these tools directly:
1276
1276
  - **decision-trace** — Record decisions
1277
1277
  - **policy-engine** — Check policies
1278
1278
  - **reflect** — Gather session artifacts
1279
- - **delegate** — Assign execution to another agent
1280
1279
 
1281
1280
  You may NEVER use:
1282
1281
  - write, write_file, create, create_file
@@ -1286,9 +1285,9 @@ You may NEVER use:
1286
1285
 
1287
1286
  ## Execution Paths After Routing
1288
1287
 
1289
- ### Direct Execution Path (via delegate tool)
1288
+ ### Direct Execution Path
1290
1289
  When workflow class is \`quick\` or \`docs-only\` (simple):
1291
- - Call \`delegate\` with agent=\`default-executor\` and mode=\`direct\`
1290
+ - Route to @default-executor with a concise task description
1292
1291
  - Choose the appropriate mode in the task description:
1293
1292
  - \`direct-stock-tools\` — for simple file changes
1294
1293
  - \`quick-answer\` — for questions
@@ -1298,26 +1297,26 @@ When workflow class is \`quick\` or \`docs-only\` (simple):
1298
1297
 
1299
1298
  ### Specialist Execution Path
1300
1299
  When workflow class is \`standard\`, \`explore\`, \`ui-heavy\`, \`bugfix\`, or \`verify-heavy\`:
1301
- - Call \`delegate\` with the role-specific specialist agent:
1302
- - \`backend-coder\` — server, API, business logic, database
1303
- - \`frontend-coder\` — UI components, client state, styling
1304
- - \`devops\` — CI/CD, deployment, infrastructure
1305
- - \`tester\` — tests, builds, verification
1306
- - \`researcher\` — API docs, library research
1307
- - \`reviewer\` — code quality review
1308
- - \`security-auditor\` — security review
1309
- - \`debug-specialist\` — root cause analysis
1300
+ - Route to the role-specific specialist agent:
1301
+ - \`@backend-coder\` — server, API, business logic, database
1302
+ - \`@frontend-coder\` — UI components, client state, styling
1303
+ - \`@devops\` — CI/CD, deployment, infrastructure
1304
+ - \`@tester\` — tests, builds, verification
1305
+ - \`@researcher\` — API docs, library research
1306
+ - \`@reviewer\` — code quality review
1307
+ - \`@security-auditor\` — security review
1308
+ - \`@debug-specialist\` — root cause analysis
1310
1309
 
1311
1310
  ### Parallel Execution Patterns
1312
1311
 
1313
1312
  Wave 1 (parallel):
1314
- delegate(agent: "researcher") — research the library API
1315
- delegate(agent: "backend-coder") — implement the model and types
1316
- delegate(agent: "tester") — write test cases
1313
+ @researcher research the library API
1314
+ @backend-coder implement the model and types
1315
+ @tester write test cases
1317
1316
 
1318
1317
  Wave 2 (after Wave 1):
1319
- delegate(agent: "backend-coder") — implement service using Wave 1 research
1320
- delegate(agent: "reviewer") — review Wave 1 implementation
1318
+ @backend-coder implement service using Wave 1 research
1319
+ @reviewer review Wave 1 implementation
1321
1320
 
1322
1321
  ## Adaptive Routing and Escalation
1323
1322
 
@@ -5744,469 +5743,6 @@ var mergeAssistTool = tool12({
5744
5743
  }
5745
5744
  });
5746
5745
 
5747
- // src/tools/delegate.ts
5748
- import { tool as tool13 } from "@opencode-ai/plugin";
5749
- import { randomUUID as randomUUID2 } from "crypto";
5750
-
5751
- // src/services/run-trace.ts
5752
- import { existsSync as existsSync16, readFileSync as readFileSync16, appendFileSync as appendFileSync4, writeFileSync as writeFileSync12, mkdirSync as mkdirSync11 } from "fs";
5753
- import { join as join16 } from "path";
5754
- import { randomUUID } from "crypto";
5755
- function runsPath(dir) {
5756
- return join16(codebaseDir(dir), "RUNS.jsonl");
5757
- }
5758
- function startTrace(dir, command, args, session_id = "session-0") {
5759
- const cd = codebaseDir(dir);
5760
- if (!existsSync16(cd))
5761
- mkdirSync11(cd, { recursive: true });
5762
- const trace = {
5763
- run_id: randomUUID(),
5764
- session_id,
5765
- command,
5766
- args,
5767
- started_at: new Date().toISOString(),
5768
- status: "running",
5769
- files_touched: [],
5770
- event_ids: [],
5771
- risk_score: 0
5772
- };
5773
- appendFileSync4(runsPath(dir), JSON.stringify(trace) + `
5774
- `, "utf-8");
5775
- return trace;
5776
- }
5777
- function loadAllTraces(dir) {
5778
- const p = runsPath(dir);
5779
- if (!existsSync16(p))
5780
- return [];
5781
- try {
5782
- return readFileSync16(p, "utf-8").trim().split(`
5783
- `).filter(Boolean).map((l) => JSON.parse(l));
5784
- } catch {
5785
- return [];
5786
- }
5787
- }
5788
- function saveAllTraces(dir, traces) {
5789
- const p = runsPath(dir);
5790
- writeFileSync12(p, traces.map((t) => JSON.stringify(t)).join(`
5791
- `) + `
5792
- `, "utf-8");
5793
- }
5794
- function endTrace(dir, run_id, status, outcome, error) {
5795
- const traces = loadAllTraces(dir);
5796
- const idx = traces.findLastIndex((t) => t.run_id === run_id);
5797
- if (idx === -1)
5798
- return;
5799
- traces[idx] = {
5800
- ...traces[idx],
5801
- ended_at: new Date().toISOString(),
5802
- status,
5803
- ...outcome ? { outcome } : {},
5804
- ...error ? { error } : {}
5805
- };
5806
- saveAllTraces(dir, traces);
5807
- }
5808
- function getTrace(dir, run_id) {
5809
- return loadAllTraces(dir).findLast((t) => t.run_id === run_id) ?? null;
5810
- }
5811
- function recordVerification(dir, run_id, kind, evidence) {
5812
- const traces = loadAllTraces(dir);
5813
- const idx = traces.findLastIndex((t) => t.run_id === run_id);
5814
- if (idx === -1)
5815
- return;
5816
- const list = traces[idx].verifications ?? [];
5817
- traces[idx] = {
5818
- ...traces[idx],
5819
- verifications: [
5820
- ...list,
5821
- { kind, evidence, timestamp: new Date().toISOString() }
5822
- ]
5823
- };
5824
- saveAllTraces(dir, traces);
5825
- }
5826
-
5827
- // src/services/delegation-budget.ts
5828
- var DEFAULT_BUDGET_CONFIG = {
5829
- maxToolCalls: 200,
5830
- maxDepth: 3,
5831
- maxSameStepRetries: 3
5832
- };
5833
- var budgets = new Map;
5834
- function allowDecision(remaining) {
5835
- return {
5836
- verdict: "allow",
5837
- reason: `Tool budget consumed; ${remaining} calls remaining`,
5838
- riskFlags: [],
5839
- source: "delegation-budget"
5840
- };
5841
- }
5842
- function denyDecision(reason) {
5843
- return {
5844
- verdict: "deny",
5845
- reason,
5846
- riskFlags: ["budget-exhausted"],
5847
- source: "delegation-budget",
5848
- escalationMessage: `[FlowDeck Budget] ${reason}. End the current step or escalate to the human.`
5849
- };
5850
- }
5851
- function resolveDelegationBudgetConfig(config) {
5852
- const incoming = config?.governance?.delegationBudget ?? {};
5853
- return {
5854
- maxToolCalls: incoming.maxToolCalls ?? DEFAULT_BUDGET_CONFIG.maxToolCalls,
5855
- maxDepth: incoming.maxDepth ?? DEFAULT_BUDGET_CONFIG.maxDepth,
5856
- maxSameStepRetries: incoming.maxSameStepRetries ?? DEFAULT_BUDGET_CONFIG.maxSameStepRetries
5857
- };
5858
- }
5859
- function init(runId, config) {
5860
- const resolved = resolveDelegationBudgetConfig(config);
5861
- const budget = {
5862
- runId,
5863
- config: resolved,
5864
- spentToolCalls: 0,
5865
- currentDepth: 0,
5866
- sameStepRetries: 0
5867
- };
5868
- budgets.set(runId, budget);
5869
- return budget;
5870
- }
5871
- function getBudget(runId) {
5872
- const budget = budgets.get(runId);
5873
- if (!budget)
5874
- return null;
5875
- return toSnapshot(budget);
5876
- }
5877
- function checkSpend(runId, _toolName) {
5878
- const budget = budgets.get(runId);
5879
- if (!budget) {
5880
- return denyDecision("No budget initialized for this run");
5881
- }
5882
- const remaining = budget.config.maxToolCalls - budget.spentToolCalls;
5883
- if (remaining <= 0) {
5884
- return denyDecision(`Tool call budget exhausted (${budget.spentToolCalls}/${budget.config.maxToolCalls})`);
5885
- }
5886
- budget.spentToolCalls += 1;
5887
- return allowDecision(Math.max(0, budget.config.maxToolCalls - budget.spentToolCalls));
5888
- }
5889
- function recordDelegation(parentRunId, childRunId) {
5890
- const parent = budgets.get(parentRunId);
5891
- if (!parent)
5892
- return false;
5893
- const child = budgets.get(childRunId) ?? init(childRunId);
5894
- child.config = parent.config;
5895
- child.currentDepth = parent.currentDepth + 1;
5896
- budgets.set(childRunId, child);
5897
- return child.currentDepth <= child.config.maxDepth;
5898
- }
5899
- function incrementSameStepRetry(runId) {
5900
- const budget = budgets.get(runId);
5901
- if (!budget)
5902
- return false;
5903
- budget.sameStepRetries += 1;
5904
- return budget.sameStepRetries <= budget.config.maxSameStepRetries;
5905
- }
5906
- function toSnapshot(budget) {
5907
- return {
5908
- runId: budget.runId,
5909
- maxToolCalls: budget.config.maxToolCalls,
5910
- maxDepth: budget.config.maxDepth,
5911
- maxSameStepRetries: budget.config.maxSameStepRetries,
5912
- spentToolCalls: budget.spentToolCalls,
5913
- currentDepth: budget.currentDepth,
5914
- sameStepRetries: budget.sameStepRetries,
5915
- remainingToolCalls: Math.max(0, budget.config.maxToolCalls - budget.spentToolCalls)
5916
- };
5917
- }
5918
-
5919
- // src/tools/delegate.ts
5920
- var DEFAULT_TIMEOUT_MS = 120000;
5921
- function normalizeMode(mode) {
5922
- if (mode === "council" || mode === "pipeline")
5923
- return mode;
5924
- return "direct";
5925
- }
5926
- function validateCouncilContext(context) {
5927
- if (!context || !Array.isArray(context.agents) || context.agents.length === 0) {
5928
- return { ok: false, error: "council mode requires context.agents to be a non-empty array of strings." };
5929
- }
5930
- for (const agent of context.agents) {
5931
- if (typeof agent !== "string") {
5932
- return { ok: false, error: "context.agents must contain only strings." };
5933
- }
5934
- }
5935
- return { ok: true, agents: context.agents };
5936
- }
5937
- function validatePipelineContext(context) {
5938
- if (!context || !Array.isArray(context.stages) || context.stages.length === 0) {
5939
- return { ok: false, error: "pipeline mode requires context.stages to be a non-empty array of { agent, task? }." };
5940
- }
5941
- for (const stage of context.stages) {
5942
- if (!stage || typeof stage !== "object" || typeof stage.agent !== "string") {
5943
- return { ok: false, error: "Each pipeline stage must be an object with a string 'agent' property." };
5944
- }
5945
- }
5946
- return { ok: true, stages: context.stages };
5947
- }
5948
- function isRegisteredAgent(agent) {
5949
- return AGENT_NAMES.includes(agent);
5950
- }
5951
- function isTextPart2(part) {
5952
- return part.type === "text";
5953
- }
5954
- function createDelegateTool(client, statePersistence, ensureRunContext) {
5955
- return tool13({
5956
- description: "Delegate work to another FlowDeck agent. Use this instead of raw @agent references for any work requiring write/edit/bash or specialist knowledge. " + "Modes: direct (single agent), council (fan out the same task to multiple agents in parallel), pipeline (sequential stages where each stage's output is passed to the next via context.previousOutput). " + "For council mode, provide context.agents (non-empty string array). For pipeline mode, provide context.stages (non-empty array of { agent, task? }).",
5957
- args: {
5958
- agent: tool13.schema.string(),
5959
- task: tool13.schema.string(),
5960
- mode: tool13.schema.enum(["direct", "council", "pipeline"]).optional().default("direct"),
5961
- context: tool13.schema.object().optional()
5962
- },
5963
- async execute(args, ctx) {
5964
- const { agent, task, mode, context } = args;
5965
- const directory = ctx.directory;
5966
- const sessionID = ctx.sessionID;
5967
- const resolvedMode = normalizeMode(mode);
5968
- let runCtx = statePersistence.getRunContext(sessionID);
5969
- if (!runCtx) {
5970
- runCtx = ensureRunContext({
5971
- sessionID,
5972
- description: task,
5973
- agent
5974
- });
5975
- }
5976
- if (!getBudget(runCtx.runId)) {
5977
- init(runCtx.runId);
5978
- }
5979
- const childRunId = randomUUID2();
5980
- const withinDepth = recordDelegation(runCtx.runId, childRunId);
5981
- if (!withinDepth) {
5982
- const snapshot = getBudget(runCtx.runId);
5983
- return {
5984
- span_id: "",
5985
- run_id: runCtx.runId,
5986
- status: "blocked",
5987
- output: "",
5988
- error: `Delegation depth limit exceeded (current: ${snapshot?.currentDepth ?? 0}, max: ${snapshot?.maxDepth ?? 0}). Escalate to the human.`
5989
- };
5990
- }
5991
- try {
5992
- if (resolvedMode === "direct") {
5993
- if (!isRegisteredAgent(agent)) {
5994
- return {
5995
- span_id: "",
5996
- run_id: runCtx.runId,
5997
- status: "blocked",
5998
- output: "",
5999
- error: `Agent "${agent}" is not a registered FlowDeck agent. Registered agents: ${AGENT_NAMES.join(", ")}.`
6000
- };
6001
- }
6002
- const span = statePersistence.openAgentSpan(runCtx, runCtx.currentSpanId, agent, task, runCtx.currentStage ?? "execute");
6003
- const result = await executeDirectDelegation(client, ctx, agent, task, span, runCtx, statePersistence, context);
6004
- statePersistence.closeAgentSpan(span.span_id, result.status === "blocked" ? "blocked" : "complete", {
6005
- output_valid: result.status === "complete"
6006
- });
6007
- return {
6008
- span_id: span.span_id,
6009
- run_id: runCtx.runId,
6010
- ...result
6011
- };
6012
- }
6013
- if (resolvedMode === "council") {
6014
- const validation = validateCouncilContext(context);
6015
- if (!validation.ok) {
6016
- return {
6017
- span_id: "",
6018
- run_id: runCtx.runId,
6019
- status: "blocked",
6020
- output: "",
6021
- error: validation.error
6022
- };
6023
- }
6024
- const unknownAgents = validation.agents.filter((a) => !isRegisteredAgent(a));
6025
- if (unknownAgents.length > 0) {
6026
- return {
6027
- span_id: "",
6028
- run_id: runCtx.runId,
6029
- status: "blocked",
6030
- output: "",
6031
- error: `Agents are not registered FlowDeck agents: ${unknownAgents.join(", ")}. Registered agents: ${AGENT_NAMES.join(", ")}.`
6032
- };
6033
- }
6034
- const results = await runCouncil(client, { directory, sessionID }, task, validation.agents, { maxConcurrency: 3 });
6035
- const spans = [];
6036
- for (const r of results) {
6037
- const span = statePersistence.openAgentSpan(runCtx, runCtx.currentSpanId, r.agent, task, runCtx.currentStage ?? "execute");
6038
- const status = r.error ? "failed" : "complete";
6039
- statePersistence.closeAgentSpan(span.span_id, status, { output_valid: status === "complete" });
6040
- spans.push({ span_id: span.span_id, status });
6041
- }
6042
- const anySuccess = results.some((r) => !r.error);
6043
- const output = results.map((r) => `--- ${r.agent} ---
6044
- ${r.error ? `ERROR: ${r.error}` : r.output}`).join(`
6045
-
6046
- `);
6047
- return {
6048
- span_id: spans[0]?.span_id ?? "",
6049
- run_id: runCtx.runId,
6050
- status: anySuccess ? "complete" : results.every((r) => r.error) ? "failed" : "blocked",
6051
- output
6052
- };
6053
- }
6054
- if (resolvedMode === "pipeline") {
6055
- const validation = validatePipelineContext(context);
6056
- if (!validation.ok) {
6057
- return {
6058
- span_id: "",
6059
- run_id: runCtx.runId,
6060
- status: "blocked",
6061
- output: "",
6062
- error: validation.error
6063
- };
6064
- }
6065
- const progress = [];
6066
- let previousOutput = "";
6067
- for (const stage of validation.stages) {
6068
- if (!isRegisteredAgent(stage.agent)) {
6069
- progress.push({
6070
- stage: stage.agent,
6071
- status: "blocked",
6072
- error: `Agent "${stage.agent}" is not a registered FlowDeck agent.`
6073
- });
6074
- return {
6075
- span_id: "",
6076
- run_id: runCtx.runId,
6077
- status: "blocked",
6078
- output: "",
6079
- error: `Agent "${stage.agent}" is not a registered FlowDeck agent.`,
6080
- pipeline_progress: progress
6081
- };
6082
- }
6083
- const span = statePersistence.openAgentSpan(runCtx, runCtx.currentSpanId, stage.agent, stage.task ?? task, runCtx.currentStage ?? "execute");
6084
- const stageContext = {
6085
- ...context,
6086
- previousOutput
6087
- };
6088
- let result;
6089
- try {
6090
- result = await executeDirectDelegation(client, ctx, stage.agent, stage.task ?? task, span, runCtx, statePersistence, stageContext);
6091
- } catch (error) {
6092
- const message = error instanceof Error ? error.message : String(error);
6093
- result = { status: "failed", output: "", error: message };
6094
- }
6095
- const spanStatus = result.status === "blocked" ? "blocked" : result.status === "failed" ? "failed" : "complete";
6096
- statePersistence.closeAgentSpan(span.span_id, spanStatus, { output_valid: spanStatus === "complete" });
6097
- progress.push({
6098
- stage: stage.agent,
6099
- status: spanStatus,
6100
- output: result.output,
6101
- error: result.error
6102
- });
6103
- if (spanStatus === "failed" || spanStatus === "blocked") {
6104
- return {
6105
- span_id: span.span_id,
6106
- run_id: runCtx.runId,
6107
- status: spanStatus,
6108
- output: result.output,
6109
- error: result.error,
6110
- pipeline_progress: progress
6111
- };
6112
- }
6113
- previousOutput = result.output;
6114
- }
6115
- return {
6116
- span_id: "",
6117
- run_id: runCtx.runId,
6118
- status: "complete",
6119
- output: previousOutput,
6120
- pipeline_progress: progress
6121
- };
6122
- }
6123
- return {
6124
- span_id: "",
6125
- run_id: runCtx.runId,
6126
- status: "blocked",
6127
- output: "",
6128
- error: `Delegation mode "${resolvedMode}" is not supported.`
6129
- };
6130
- } catch (error) {
6131
- const message = error instanceof Error ? error.message : String(error);
6132
- return {
6133
- span_id: "",
6134
- run_id: runCtx.runId,
6135
- status: "failed",
6136
- output: "",
6137
- error: message
6138
- };
6139
- }
6140
- }
6141
- });
6142
- }
6143
- function extractErrorMessage(error) {
6144
- if (error instanceof Error)
6145
- return error.message;
6146
- if (typeof error === "string")
6147
- return error;
6148
- if (error && typeof error === "object" && "message" in error)
6149
- return String(error.message);
6150
- return String(error);
6151
- }
6152
- async function executeDirectDelegation(client, ctx, agent, task, span, runCtx, statePersistence, context) {
6153
- const createRes = await client.session.create({
6154
- body: { parentID: ctx.sessionID, title: `Delegate: ${agent}` },
6155
- query: { directory: ctx.directory }
6156
- });
6157
- if (createRes.error || !createRes.data?.id) {
6158
- return {
6159
- status: "failed",
6160
- output: "",
6161
- error: extractErrorMessage(createRes.error ?? "Failed to create delegate session")
6162
- };
6163
- }
6164
- const childId = createRes.data.id;
6165
- statePersistence.updateRunContext({
6166
- ...runCtx,
6167
- sessionID: childId,
6168
- currentSpanId: span.span_id
6169
- });
6170
- const parts = [
6171
- { type: "text", text: buildDelegatePrompt(agent, task, context) }
6172
- ];
6173
- const promptRes = await Promise.race([
6174
- client.session.prompt({
6175
- path: { id: childId },
6176
- body: { agent, parts },
6177
- query: { directory: ctx.directory }
6178
- }),
6179
- new Promise((_, reject) => setTimeout(() => reject(new Error("Delegate session timed out")), DEFAULT_TIMEOUT_MS))
6180
- ]);
6181
- if (promptRes.error) {
6182
- return {
6183
- status: "failed",
6184
- output: "",
6185
- error: extractErrorMessage(promptRes.error)
6186
- };
6187
- }
6188
- const output = (promptRes.data?.parts ?? []).filter(isTextPart2).map((p) => p.text).join(`
6189
- `);
6190
- return {
6191
- status: "complete",
6192
- output: output || "(no output)"
6193
- };
6194
- }
6195
- function buildDelegatePrompt(agent, task, context) {
6196
- const lines = [
6197
- `You are @${agent}.`,
6198
- "",
6199
- "Task:",
6200
- task
6201
- ];
6202
- if (context && Object.keys(context).length > 0) {
6203
- lines.push("", "Context:", JSON.stringify(context, null, 2));
6204
- }
6205
- lines.push("", "Do the work above. Return a concise summary of what you did and any results.");
6206
- return lines.join(`
6207
- `);
6208
- }
6209
-
6210
5746
  // src/hooks/notifications.ts
6211
5747
  import { execFile } from "child_process";
6212
5748
  var INTERACTIVE_COMMANDS = new Set([
@@ -6327,13 +5863,13 @@ class NotificationController {
6327
5863
  return this.lastNotifiedKey;
6328
5864
  }
6329
5865
  }
6330
- function notifyPermissionNeeded(tool14) {
6331
- notify("FlowDeck Permission Required", `Agent needs approval to use tool: ${tool14}`, "critical");
5866
+ function notifyPermissionNeeded(tool13) {
5867
+ notify("FlowDeck Permission Required", `Agent needs approval to use tool: ${tool13}`, "critical");
6332
5868
  }
6333
5869
 
6334
5870
  // src/hooks/shell-env-hook.ts
6335
- import { existsSync as existsSync17, readFileSync as readFileSync17 } from "fs";
6336
- import { join as join17 } from "path";
5871
+ import { existsSync as existsSync16, readFileSync as readFileSync16 } from "fs";
5872
+ import { join as join16 } from "path";
6337
5873
  import { createRequire } from "module";
6338
5874
  var _version;
6339
5875
  function getVersion() {
@@ -6369,7 +5905,7 @@ var MARKER_TO_LANG = {
6369
5905
  };
6370
5906
  function detectPackageManager(root) {
6371
5907
  for (const [lockfile, pm] of Object.entries(LOCKFILE_TO_PM)) {
6372
- if (existsSync17(join17(root, lockfile)))
5908
+ if (existsSync16(join16(root, lockfile)))
6373
5909
  return pm;
6374
5910
  }
6375
5911
  return;
@@ -6378,7 +5914,7 @@ function detectLanguages(root) {
6378
5914
  const langs = [];
6379
5915
  const seen = new Set;
6380
5916
  for (const [marker, lang] of Object.entries(MARKER_TO_LANG)) {
6381
- if (!seen.has(lang) && existsSync17(join17(root, marker))) {
5917
+ if (!seen.has(lang) && existsSync16(join16(root, marker))) {
6382
5918
  langs.push(lang);
6383
5919
  seen.add(lang);
6384
5920
  }
@@ -6386,11 +5922,11 @@ function detectLanguages(root) {
6386
5922
  return langs;
6387
5923
  }
6388
5924
  function readCurrentPhase(root) {
6389
- const statePath3 = join17(root, ".planning", "STATE.md");
6390
- if (!existsSync17(statePath3))
5925
+ const statePath3 = join16(root, ".planning", "STATE.md");
5926
+ if (!existsSync16(statePath3))
6391
5927
  return;
6392
5928
  try {
6393
- const content = readFileSync17(statePath3, "utf-8");
5929
+ const content = readFileSync16(statePath3, "utf-8");
6394
5930
  const match = content.match(/phase:\s*(\S+)/i);
6395
5931
  return match?.[1];
6396
5932
  } catch {
@@ -6495,8 +6031,8 @@ function createSessionIdleHook(client, tracker) {
6495
6031
  }
6496
6032
 
6497
6033
  // src/hooks/compaction-hook.ts
6498
- import { existsSync as existsSync18, readFileSync as readFileSync18 } from "fs";
6499
- import { join as join18 } from "path";
6034
+ import { existsSync as existsSync17, readFileSync as readFileSync17 } from "fs";
6035
+ import { join as join17 } from "path";
6500
6036
  var STRUCTURED_SUMMARY_PROMPT = `
6501
6037
  When summarizing this session, you MUST include the following sections:
6502
6038
 
@@ -6537,10 +6073,10 @@ For each: agent name, status, description, session_id.
6537
6073
  var _lastInjected = new Map;
6538
6074
  function readPlanningState2(directory) {
6539
6075
  const sp = statePath(directory);
6540
- if (!existsSync18(sp))
6076
+ if (!existsSync17(sp))
6541
6077
  return null;
6542
6078
  try {
6543
- const content = readFileSync18(sp, "utf-8");
6079
+ const content = readFileSync17(sp, "utf-8");
6544
6080
  const parsed = parseState(content);
6545
6081
  const version = typeof parsed.summaryVersion === "number" ? parsed.summaryVersion : 0;
6546
6082
  return { content: content.slice(0, 1500), version };
@@ -6577,15 +6113,15 @@ function createCompactionHook(ctx, tracker, promptFragment) {
6577
6113
  sections.push(`_State unchanged since last compaction. summaryVersion=${currentStateVersion}_`);
6578
6114
  sections.push("");
6579
6115
  }
6580
- const indexPath2 = join18(ctx.directory, ".planning", "CODEBASE_INDEX.md");
6581
- if (indexChanged && existsSync18(indexPath2)) {
6116
+ const indexPath2 = join17(ctx.directory, ".planning", "CODEBASE_INDEX.md");
6117
+ if (indexChanged && existsSync17(indexPath2)) {
6582
6118
  try {
6583
- const indexContent = readFileSync18(indexPath2, "utf-8");
6119
+ const indexContent = readFileSync17(indexPath2, "utf-8");
6584
6120
  const indexSummary = "\n## Codebase Index\n```\n" + indexContent.slice(0, 800) + "\n```\n";
6585
6121
  sections.push(indexSummary);
6586
6122
  sections.push("");
6587
6123
  } catch {}
6588
- } else if (existsSync18(indexPath2)) {
6124
+ } else if (existsSync17(indexPath2)) {
6589
6125
  sections.push(`## Codebase Index (unchanged, v${currentIndexVersion})`);
6590
6126
  sections.push(`_Index unchanged since last compaction. summaryVersion=${currentIndexVersion}_`);
6591
6127
  sections.push("");
@@ -6762,23 +6298,23 @@ function createFlowDeckMcps() {
6762
6298
  }
6763
6299
 
6764
6300
  // src/config/loader.ts
6765
- import { existsSync as existsSync19, readFileSync as readFileSync19 } from "fs";
6766
- import { join as join19 } from "path";
6301
+ import { existsSync as existsSync18, readFileSync as readFileSync18 } from "fs";
6302
+ import { join as join18 } from "path";
6767
6303
  import { homedir } from "os";
6768
6304
  var CONFIG_FILENAME = "flowdeck.json";
6769
6305
  function getGlobalConfigDir() {
6770
- return process.env.OPENCODE_CONFIG_DIR || (process.env.XDG_CONFIG_HOME ? join19(process.env.XDG_CONFIG_HOME, "opencode") : join19(homedir(), ".config", "opencode"));
6306
+ return process.env.OPENCODE_CONFIG_DIR || (process.env.XDG_CONFIG_HOME ? join18(process.env.XDG_CONFIG_HOME, "opencode") : join18(homedir(), ".config", "opencode"));
6771
6307
  }
6772
6308
  function loadFlowDeckConfig(directory) {
6773
6309
  const candidates = [];
6774
6310
  if (directory) {
6775
- candidates.push(join19(directory, ".opencode", CONFIG_FILENAME));
6311
+ candidates.push(join18(directory, ".opencode", CONFIG_FILENAME));
6776
6312
  }
6777
- candidates.push(join19(getGlobalConfigDir(), CONFIG_FILENAME));
6313
+ candidates.push(join18(getGlobalConfigDir(), CONFIG_FILENAME));
6778
6314
  for (const configPath of candidates) {
6779
- if (existsSync19(configPath)) {
6315
+ if (existsSync18(configPath)) {
6780
6316
  try {
6781
- const content = readFileSync19(configPath, "utf-8");
6317
+ const content = readFileSync18(configPath, "utf-8");
6782
6318
  return JSON.parse(content);
6783
6319
  } catch {}
6784
6320
  }
@@ -6801,8 +6337,8 @@ function resolveDesignFirstConfig(config) {
6801
6337
  };
6802
6338
  }
6803
6339
  // src/services/context-ingress.ts
6804
- import { existsSync as existsSync20, readFileSync as readFileSync20, readdirSync as readdirSync3, statSync, mkdirSync as mkdirSync12, writeFileSync as writeFileSync13 } from "fs";
6805
- import { join as join20, basename as basename2 } from "path";
6340
+ import { existsSync as existsSync19, readFileSync as readFileSync19, readdirSync as readdirSync3, statSync, mkdirSync as mkdirSync11, writeFileSync as writeFileSync12 } from "fs";
6341
+ import { join as join19, basename as basename2 } from "path";
6806
6342
  import { fileURLToPath as fileURLToPath2 } from "url";
6807
6343
  import { dirname as dirname3 } from "path";
6808
6344
 
@@ -6927,8 +6463,8 @@ function computeLanguageSnapshot(projectRoot) {
6927
6463
  let maxMtime = 0;
6928
6464
  const present = [];
6929
6465
  for (const file of INDICATOR_FILES) {
6930
- const path = join20(projectRoot, file);
6931
- if (existsSync20(path)) {
6466
+ const path = join19(projectRoot, file);
6467
+ if (existsSync19(path)) {
6932
6468
  present.push(file);
6933
6469
  try {
6934
6470
  const stat = statSync(path);
@@ -6953,14 +6489,14 @@ function getCachedLanguages(projectRoot) {
6953
6489
  function computeSkillsSnapshot(skillsDir) {
6954
6490
  let maxMtime = 0;
6955
6491
  const entries = [];
6956
- if (existsSync20(skillsDir)) {
6492
+ if (existsSync19(skillsDir)) {
6957
6493
  try {
6958
6494
  for (const entry of readdirSync3(skillsDir, { withFileTypes: true })) {
6959
6495
  if (!entry.isDirectory())
6960
6496
  continue;
6961
6497
  entries.push(entry.name);
6962
6498
  try {
6963
- const stat = statSync(join20(skillsDir, entry.name));
6499
+ const stat = statSync(join19(skillsDir, entry.name));
6964
6500
  if (stat.mtimeMs > maxMtime) {
6965
6501
  maxMtime = stat.mtimeMs;
6966
6502
  }
@@ -6977,7 +6513,7 @@ function getCachedSkillNames(skillsDir) {
6977
6513
  return cached.names;
6978
6514
  }
6979
6515
  const names = [];
6980
- if (existsSync20(skillsDir)) {
6516
+ if (existsSync19(skillsDir)) {
6981
6517
  try {
6982
6518
  for (const entry of readdirSync3(skillsDir, { withFileTypes: true })) {
6983
6519
  if (entry.isDirectory()) {
@@ -7161,9 +6697,9 @@ class ContextIngressService {
7161
6697
  }
7162
6698
  persistBudgetSnapshot(ctx) {
7163
6699
  try {
7164
- const cacheDir = join20(ctx.directory, ".planning", "cache");
7165
- if (!existsSync20(cacheDir)) {
7166
- mkdirSync12(cacheDir, { recursive: true });
6700
+ const cacheDir = join19(ctx.directory, ".planning", "cache");
6701
+ if (!existsSync19(cacheDir)) {
6702
+ mkdirSync11(cacheDir, { recursive: true });
7167
6703
  }
7168
6704
  const snapshot = {
7169
6705
  sessionID: ctx.sessionID,
@@ -7173,7 +6709,7 @@ class ContextIngressService {
7173
6709
  componentBudgets: this.getComponentBudgets(ctx),
7174
6710
  writtenAt: new Date().toISOString()
7175
6711
  };
7176
- writeFileSync13(join20(cacheDir, "latest-context.json"), JSON.stringify(snapshot, null, 2), "utf-8");
6712
+ writeFileSync12(join19(cacheDir, "latest-context.json"), JSON.stringify(snapshot, null, 2), "utf-8");
7177
6713
  } catch (error) {
7178
6714
  const message = error instanceof Error ? error.message : String(error);
7179
6715
  console.error(`[context-ingress] failed to persist budget snapshot: ${message}`);
@@ -7279,12 +6815,12 @@ class ContextIngressService {
7279
6815
  return this._assembledBySession.get(sessionID);
7280
6816
  }
7281
6817
  readPlanContent(projectRoot, state) {
7282
- const planningDir2 = join20(projectRoot, ".planning");
7283
- const planPath = join20(planningDir2, "PLAN.md");
7284
- if (!existsSync20(planPath))
6818
+ const planningDir2 = join19(projectRoot, ".planning");
6819
+ const planPath = join19(planningDir2, "PLAN.md");
6820
+ if (!existsSync19(planPath))
7285
6821
  return "";
7286
6822
  try {
7287
- let content = readFileSync20(planPath, "utf-8");
6823
+ let content = readFileSync19(planPath, "utf-8");
7288
6824
  if (content.length > this.options.planTruncateThreshold) {
7289
6825
  content = `${content.slice(0, this.options.planTruncateTo)}
7290
6826
 
@@ -7296,28 +6832,28 @@ class ContextIngressService {
7296
6832
  }
7297
6833
  }
7298
6834
  readCodebaseDocs(projectRoot) {
7299
- const codebaseDir2 = join20(projectRoot, ".codebase");
7300
- if (!existsSync20(codebaseDir2))
6835
+ const codebaseDir2 = join19(projectRoot, ".codebase");
6836
+ if (!existsSync19(codebaseDir2))
7301
6837
  return {};
7302
6838
  const docs = {};
7303
6839
  try {
7304
6840
  for (const file of readdirSync3(codebaseDir2)) {
7305
6841
  if (!file.endsWith(".md"))
7306
6842
  continue;
7307
- const filePath = join20(codebaseDir2, file);
6843
+ const filePath = join19(codebaseDir2, file);
7308
6844
  try {
7309
- docs[file] = readFileSync20(filePath, "utf-8");
6845
+ docs[file] = readFileSync19(filePath, "utf-8");
7310
6846
  } catch {}
7311
6847
  }
7312
6848
  } catch {}
7313
6849
  return docs;
7314
6850
  }
7315
6851
  readRecentEvents(projectRoot) {
7316
- const eventsPath = join20(projectRoot, ".opencode", "flowdeck-events.jsonl");
7317
- if (!existsSync20(eventsPath))
6852
+ const eventsPath = join19(projectRoot, ".opencode", "flowdeck-events.jsonl");
6853
+ if (!existsSync19(eventsPath))
7318
6854
  return [];
7319
6855
  try {
7320
- const content = readFileSync20(eventsPath, "utf-8");
6856
+ const content = readFileSync19(eventsPath, "utf-8");
7321
6857
  const cutoff = Date.now() - this.options.eventMaxAgeMinutes * 60 * 1000;
7322
6858
  const events = [];
7323
6859
  for (const line of content.split(`
@@ -7378,8 +6914,9 @@ class ContextIngressService {
7378
6914
  }
7379
6915
  selectRelevantRules(projectRoot, description, state, route, currentStage) {
7380
6916
  const __dir = dirname3(fileURLToPath2(import.meta.url));
7381
- const rulesDir = join20(__dir, "..", "rules");
7382
- if (!existsSync20(rulesDir))
6917
+ const rulesDir = join19(__dir, "..", "rules");
6918
+ const sharedDir = join19(__dir, "..", "agents", "shared");
6919
+ if (!existsSync19(rulesDir))
7383
6920
  return [];
7384
6921
  const stage = currentStage ?? this.inferStageFromRoute(route);
7385
6922
  const languages = getCachedLanguages(projectRoot);
@@ -7391,6 +6928,17 @@ class ContextIngressService {
7391
6928
  }
7392
6929
  const seen = new Set;
7393
6930
  const result = [];
6931
+ if (existsSync19(sharedDir)) {
6932
+ for (const file of readdirSync3(sharedDir)) {
6933
+ if (!file.endsWith(".md"))
6934
+ continue;
6935
+ const path = join19(sharedDir, file);
6936
+ if (seen.has(path))
6937
+ continue;
6938
+ seen.add(path);
6939
+ result.push(path);
6940
+ }
6941
+ }
7394
6942
  for (const rule of selection.selected) {
7395
6943
  const name = basename2(rule.path);
7396
6944
  if (seen.has(name))
@@ -7405,8 +6953,8 @@ class ContextIngressService {
7405
6953
  }
7406
6954
  selectRelevantSkills(description) {
7407
6955
  const __dir = dirname3(fileURLToPath2(import.meta.url));
7408
- const skillsDir = join20(__dir, "..", "skills");
7409
- if (!existsSync20(skillsDir))
6956
+ const skillsDir = join19(__dir, "..", "skills");
6957
+ if (!existsSync19(skillsDir))
7410
6958
  return [];
7411
6959
  const names = getCachedSkillNames(skillsDir);
7412
6960
  return scoreSkills(names, description);
@@ -7417,7 +6965,7 @@ function createContextIngressService(options) {
7417
6965
  }
7418
6966
 
7419
6967
  // src/services/harness-policy.ts
7420
- import { randomUUID as randomUUID6 } from "crypto";
6968
+ import { randomUUID as randomUUID4 } from "crypto";
7421
6969
 
7422
6970
  // src/services/loop-detector.ts
7423
6971
  import { resolve as resolve2 } from "path";
@@ -7504,39 +7052,39 @@ function collapseWhitespace(input) {
7504
7052
  return input.replace(/\s+/g, " ").trim();
7505
7053
  }
7506
7054
  function normalizeAction(toolName, args) {
7507
- const tool14 = toolName.toLowerCase();
7508
- if (tool14 === "bash" || tool14 === "shell") {
7055
+ const tool13 = toolName.toLowerCase();
7056
+ if (tool13 === "bash" || tool13 === "shell") {
7509
7057
  const command = typeof args.command === "string" ? args.command : "";
7510
7058
  const normalized = collapseWhitespace(resolveEnvVars(command)).toLowerCase();
7511
7059
  return `shell:${normalized}`;
7512
7060
  }
7513
- if (tool14 === "read" || tool14 === "view") {
7061
+ if (tool13 === "read" || tool13 === "view") {
7514
7062
  const filePath = typeof args.filePath === "string" ? args.filePath : "";
7515
7063
  try {
7516
- return `${tool14}:${resolve2(filePath || "")}`;
7064
+ return `${tool13}:${resolve2(filePath || "")}`;
7517
7065
  } catch {
7518
- return `${tool14}:${filePath}`;
7066
+ return `${tool13}:${filePath}`;
7519
7067
  }
7520
7068
  }
7521
- if (tool14 === "write" || tool14 === "edit") {
7069
+ if (tool13 === "write" || tool13 === "edit") {
7522
7070
  const filePath = typeof args.filePath === "string" ? args.filePath : "";
7523
7071
  try {
7524
- return `${tool14}:${resolve2(filePath || "")}`;
7072
+ return `${tool13}:${resolve2(filePath || "")}`;
7525
7073
  } catch {
7526
- return `${tool14}:${filePath}`;
7074
+ return `${tool13}:${filePath}`;
7527
7075
  }
7528
7076
  }
7529
- if (tool14 === "grep" || tool14 === "glob" || tool14 === "search") {
7077
+ if (tool13 === "grep" || tool13 === "glob" || tool13 === "search") {
7530
7078
  const pattern = typeof args.pattern === "string" ? args.pattern : "";
7531
7079
  const path = typeof args.path === "string" ? args.path : "";
7532
- return `${tool14}:${pattern}:${resolve2(path || ".")}`;
7080
+ return `${tool13}:${pattern}:${resolve2(path || ".")}`;
7533
7081
  }
7534
7082
  const sorted = stableStringify(args);
7535
- return `${tool14}:${sorted}`;
7083
+ return `${tool13}:${sorted}`;
7536
7084
  }
7537
7085
  function classifyObservation(toolName, previous, output, status, similarityThreshold) {
7538
7086
  const outputPreview = getOutputPreview(output);
7539
- const tool14 = toolName.toLowerCase();
7087
+ const tool13 = toolName.toLowerCase();
7540
7088
  if (status === "blocked") {
7541
7089
  return { observation: "same_result", outputHash: hashOutput(output), outputPreview };
7542
7090
  }
@@ -7550,7 +7098,7 @@ function classifyObservation(toolName, previous, output, status, similarityThres
7550
7098
  outputPreview: errorMessage.slice(0, 200)
7551
7099
  };
7552
7100
  }
7553
- if (tool14 === "write" || tool14 === "edit") {
7101
+ if (tool13 === "write" || tool13 === "edit") {
7554
7102
  const contentHash = hashOutput(output);
7555
7103
  return { observation: "new_information", outputHash: contentHash, outputPreview };
7556
7104
  }
@@ -7561,7 +7109,7 @@ function classifyObservation(toolName, previous, output, status, similarityThres
7561
7109
  if (outputHash === previous.outputHash) {
7562
7110
  return { observation: "same_result", outputHash, outputPreview };
7563
7111
  }
7564
- if (NON_MUTATING_TOOLS.has(tool14)) {
7112
+ if (NON_MUTATING_TOOLS.has(tool13)) {
7565
7113
  const similarity = lineSimilarity(outputPreview, previous.outputPreview);
7566
7114
  if (similarity >= similarityThreshold) {
7567
7115
  return { observation: "no_progress", outputHash, outputPreview };
@@ -7570,25 +7118,25 @@ function classifyObservation(toolName, previous, output, status, similarityThres
7570
7118
  return { observation: "new_information", outputHash, outputPreview };
7571
7119
  }
7572
7120
  function redactForDisplay(toolName, normalizedKey) {
7573
- const tool14 = toolName.toLowerCase();
7574
- if (tool14 === "bash" || tool14 === "shell") {
7121
+ const tool13 = toolName.toLowerCase();
7122
+ if (tool13 === "bash" || tool13 === "shell") {
7575
7123
  const idx2 = normalizedKey.indexOf(":");
7576
7124
  const cmd = idx2 >= 0 ? normalizedKey.slice(idx2 + 1) : normalizedKey;
7577
7125
  const preview = cmd.slice(0, 30);
7578
7126
  const hash = djb2Hash(cmd);
7579
- return `${tool14}:"${preview}" (hash: ${hash})`;
7127
+ return `${tool13}:"${preview}" (hash: ${hash})`;
7580
7128
  }
7581
7129
  const idx = normalizedKey.indexOf(":");
7582
7130
  if (idx >= 0) {
7583
7131
  const body = normalizedKey.slice(idx + 1);
7584
7132
  if (body.startsWith("/") || body.startsWith(".") || body.includes("/")) {
7585
- return `${tool14}:"${body}"`;
7133
+ return `${tool13}:"${body}"`;
7586
7134
  }
7587
7135
  const preview = body.slice(0, 30);
7588
7136
  const hash = djb2Hash(body);
7589
- return `${tool14}:"${preview}" (hash: ${hash})`;
7137
+ return `${tool13}:"${preview}" (hash: ${hash})`;
7590
7138
  }
7591
- return `${tool14}:"${normalizedKey}"`;
7139
+ return `${tool13}:"${normalizedKey}"`;
7592
7140
  }
7593
7141
 
7594
7142
  class LoopDetector {
@@ -7766,8 +7314,7 @@ var CONTRACTS = [
7766
7314
  "load-rules",
7767
7315
  "list-rules",
7768
7316
  "hash-edit",
7769
- "failure-replay",
7770
- "delegate"
7317
+ "failure-replay"
7771
7318
  ],
7772
7319
  forbiddenActions: [
7773
7320
  "write_file",
@@ -8271,13 +7818,13 @@ function resolveSupervisorConfig(directory) {
8271
7818
  function isRegisteredCommand(name) {
8272
7819
  return REGISTERED_COMMANDS.includes(name);
8273
7820
  }
8274
- function isRegisteredAgent2(name) {
7821
+ function isRegisteredAgent(name) {
8275
7822
  return AGENT_NAMES.includes(name);
8276
7823
  }
8277
7824
  function isRegisteredTarget(name) {
8278
7825
  if (isRegisteredCommand(name))
8279
7826
  return { exists: true, type: "command" };
8280
- if (isRegisteredAgent2(name))
7827
+ if (isRegisteredAgent(name))
8281
7828
  return { exists: true, type: "agent" };
8282
7829
  return { exists: false, type: "agent" };
8283
7830
  }
@@ -8519,8 +8066,8 @@ function reviewToolCall(directory, input) {
8519
8066
  }
8520
8067
 
8521
8068
  // src/hooks/tool-guard.ts
8522
- import { existsSync as existsSync21, readFileSync as readFileSync21 } from "fs";
8523
- import { join as join21 } from "path";
8069
+ import { existsSync as existsSync20, readFileSync as readFileSync20 } from "fs";
8070
+ import { join as join20 } from "path";
8524
8071
 
8525
8072
  // src/lib/task-routing.ts
8526
8073
  var UI_HEAVY_KEYWORDS = [
@@ -8632,23 +8179,23 @@ function isBashCommandDangerous(cmd) {
8632
8179
  }
8633
8180
  return null;
8634
8181
  }
8635
- function isBlocked(tool14, args) {
8636
- const patterns = BLOCKED_PATTERNS[tool14];
8182
+ function isBlocked(tool13, args) {
8183
+ const patterns = BLOCKED_PATTERNS[tool13];
8637
8184
  if (!patterns)
8638
8185
  return null;
8639
- if (tool14 === "bash") {
8186
+ if (tool13 === "bash") {
8640
8187
  const cmd = args.command;
8641
8188
  if (!cmd)
8642
8189
  return null;
8643
8190
  return isBashCommandDangerous(cmd);
8644
8191
  }
8645
- if (tool14 === "read" || tool14 === "write") {
8192
+ if (tool13 === "read" || tool13 === "write") {
8646
8193
  const filePath = extractTargetPath(args);
8647
8194
  if (!filePath)
8648
8195
  return null;
8649
8196
  for (const p of patterns) {
8650
8197
  if (filePath.includes(p)) {
8651
- return tool14 === "read" ? `FLOWDECK: Access to "${p}" files is blocked.` : `FLOWDECK: Writing to "${p}" is blocked.`;
8198
+ return tool13 === "read" ? `FLOWDECK: Access to "${p}" files is blocked.` : `FLOWDECK: Writing to "${p}" is blocked.`;
8652
8199
  }
8653
8200
  }
8654
8201
  return null;
@@ -8656,11 +8203,11 @@ function isBlocked(tool14, args) {
8656
8203
  return null;
8657
8204
  }
8658
8205
  function checkArchConstraint(directory, filePath) {
8659
- const constraintsPath = join21(codebaseDir(directory), "CONSTRAINTS.md");
8660
- if (!existsSync21(constraintsPath))
8206
+ const constraintsPath = join20(codebaseDir(directory), "CONSTRAINTS.md");
8207
+ if (!existsSync20(constraintsPath))
8661
8208
  return null;
8662
8209
  try {
8663
- const content = readFileSync21(constraintsPath, "utf-8");
8210
+ const content = readFileSync20(constraintsPath, "utf-8");
8664
8211
  const match = content.match(/## Forbidden Paths\n([\s\S]*?)(?:\n##|$)/);
8665
8212
  if (!match)
8666
8213
  return null;
@@ -8701,9 +8248,9 @@ function isUiDesignApprovalRequired(directory) {
8701
8248
  return !(state.design_stage === "handoff_complete" && state.design_approved);
8702
8249
  }
8703
8250
  const planPath = phasePlanPath(directory, state.phase || 1);
8704
- if (!existsSync21(planPath))
8251
+ if (!existsSync20(planPath))
8705
8252
  return false;
8706
- const planContent = readFileSync21(planPath, "utf-8");
8253
+ const planContent = readFileSync20(planPath, "utf-8");
8707
8254
  if (!isUiHeavyTask(planContent))
8708
8255
  return false;
8709
8256
  return !(state.design_stage === "handoff_complete" && state.design_approved);
@@ -8721,15 +8268,15 @@ function deny(reason, escalationMessage, riskFlags = []) {
8721
8268
  };
8722
8269
  }
8723
8270
  function evaluate(input) {
8724
- const { directory, tool: tool14, args } = input;
8725
- if (tool14 !== "bash" && tool14 !== "read" && tool14 !== "write" && tool14 !== "edit") {
8271
+ const { directory, tool: tool13, args } = input;
8272
+ if (tool13 !== "bash" && tool13 !== "read" && tool13 !== "write" && tool13 !== "edit") {
8726
8273
  return allow("Tool is not guardable");
8727
8274
  }
8728
- const blocked = isBlocked(tool14, args);
8275
+ const blocked = isBlocked(tool13, args);
8729
8276
  if (blocked) {
8730
8277
  return deny(blocked, blocked, ["dangerous-pattern"]);
8731
8278
  }
8732
- if (tool14 === "write" || tool14 === "edit") {
8279
+ if (tool13 === "write" || tool13 === "edit") {
8733
8280
  const phaseBlock = checkPhaseEnforcement(directory);
8734
8281
  if (phaseBlock) {
8735
8282
  const isAdvisory = phaseBlock.includes("[design-gate]: advisory");
@@ -8748,15 +8295,15 @@ function evaluate(input) {
8748
8295
  }
8749
8296
 
8750
8297
  // src/hooks/guard-rails.ts
8751
- import { existsSync as existsSync22, readFileSync as readFileSync22 } from "fs";
8752
- import { join as join22 } from "path";
8298
+ import { existsSync as existsSync21, readFileSync as readFileSync21 } from "fs";
8299
+ import { join as join21 } from "path";
8753
8300
  var PLANNING_DIR2 = ".planning";
8754
8301
  var CONFIG_FILE = "config.json";
8755
8302
  var STATE_FILE2 = "STATE.md";
8756
8303
  function resolveExecutionMode(configPath, trustScore, volatility) {
8757
- if (existsSync22(configPath)) {
8304
+ if (existsSync21(configPath)) {
8758
8305
  try {
8759
- const config = JSON.parse(readFileSync22(configPath, "utf-8"));
8306
+ const config = JSON.parse(readFileSync21(configPath, "utf-8"));
8760
8307
  if (config.execution_mode === "review-only")
8761
8308
  return "review-only";
8762
8309
  if (config.execution_mode === "guarded")
@@ -8818,24 +8365,24 @@ function deny2(reason, escalationMessage, riskFlags = []) {
8818
8365
  };
8819
8366
  }
8820
8367
  function evaluate2(input) {
8821
- const { directory, tool: tool14, args } = input;
8822
- const planningDirPath = join22(directory, PLANNING_DIR2);
8368
+ const { directory, tool: tool13, args } = input;
8369
+ const planningDirPath = join21(directory, PLANNING_DIR2);
8823
8370
  const codebaseDirectory = codebaseDir(directory);
8824
- const configPath = join22(planningDirPath, CONFIG_FILE);
8825
- const statePath3 = join22(planningDirPath, STATE_FILE2);
8371
+ const configPath = join21(planningDirPath, CONFIG_FILE);
8372
+ const statePath3 = join21(planningDirPath, STATE_FILE2);
8826
8373
  const workspaceRoot = findWorkspaceRoot(directory);
8827
8374
  if (workspaceRoot && directory !== workspaceRoot) {
8828
8375
  const workspaceConfig = getWorkspaceConfig(directory);
8829
- if (workspaceConfig && workspaceConfig.workspace_mode === "shared" && !existsSync22(planningDirPath)) {
8376
+ if (workspaceConfig && workspaceConfig.workspace_mode === "shared" && !existsSync21(planningDirPath)) {
8830
8377
  const msg = `No .planning/ in this sub-repo. Switch to workspace root: cd ${workspaceRoot}`;
8831
8378
  return deny2(msg, `[flowdeck] BLOCK: ${msg}`, ["workspace-shared-mode"]);
8832
8379
  }
8833
8380
  }
8834
- if (tool14 === "write" || tool14 === "edit") {
8835
- if (!existsSync22(planningDirPath)) {
8381
+ if (tool13 === "write" || tool13 === "edit") {
8382
+ if (!existsSync21(planningDirPath)) {
8836
8383
  return allow2("FlowDeck not initialized in this directory — skipping guard-rails");
8837
8384
  }
8838
- if (!existsSync22(codebaseDirectory)) {
8385
+ if (!existsSync21(codebaseDirectory)) {
8839
8386
  const msg = ".codebase/ not found. Run /fd-map-codebase to map the codebase.";
8840
8387
  return allow2(msg, ["codebase-missing"]);
8841
8388
  }
@@ -8865,7 +8412,7 @@ function evaluate2(input) {
8865
8412
  const blockMessage = getBlockMessage(planningDirPath);
8866
8413
  return deny2(blockMessage, `[flowdeck] BLOCK: ${blockMessage}`, ["plan-not-confirmed"]);
8867
8414
  }
8868
- if (tool14 === "bash") {
8415
+ if (tool13 === "bash") {
8869
8416
  const cmd = String(args?.command || "");
8870
8417
  for (const pattern of BUILD_DEPLOY_PATTERNS) {
8871
8418
  if (cmd.includes(pattern)) {
@@ -8899,15 +8446,15 @@ function getDesignGateMessage(dir) {
8899
8446
  }
8900
8447
  function planSuggestsUiHeavy(dir, phase) {
8901
8448
  const planPath = phasePlanPath(dir, phase);
8902
- if (!existsSync22(planPath))
8449
+ if (!existsSync21(planPath))
8903
8450
  return false;
8904
- const planContent = readFileSync22(planPath, "utf-8");
8451
+ const planContent = readFileSync21(planPath, "utf-8");
8905
8452
  return isUiHeavyTask(planContent);
8906
8453
  }
8907
8454
  function effectiveSeverity(configPath, statePath3) {
8908
- if (existsSync22(configPath)) {
8455
+ if (existsSync21(configPath)) {
8909
8456
  try {
8910
- const configContent = readFileSync22(configPath, "utf-8");
8457
+ const configContent = readFileSync21(configPath, "utf-8");
8911
8458
  const config = JSON.parse(configContent);
8912
8459
  if (config.guard_enforcement === "warn")
8913
8460
  return "warn";
@@ -8920,10 +8467,10 @@ function effectiveSeverity(configPath, statePath3) {
8920
8467
  return getPlanConfirmed(statePath3) ? "block" : "warn";
8921
8468
  }
8922
8469
  function getPlanConfirmed(statePath3) {
8923
- if (!existsSync22(statePath3))
8470
+ if (!existsSync21(statePath3))
8924
8471
  return false;
8925
8472
  try {
8926
- const content = readFileSync22(statePath3, "utf-8");
8473
+ const content = readFileSync21(statePath3, "utf-8");
8927
8474
  const match = content.match(/plan_confirmed:\s*(true|false)/i);
8928
8475
  return match ? match[1].toLowerCase() === "true" : false;
8929
8476
  } catch {
@@ -8931,23 +8478,23 @@ function getPlanConfirmed(statePath3) {
8931
8478
  }
8932
8479
  }
8933
8480
  function getWarningMessage(planningDir2) {
8934
- if (!existsSync22(join22(planningDir2, STATE_FILE2))) {
8481
+ if (!existsSync21(join21(planningDir2, STATE_FILE2))) {
8935
8482
  return "No STATE.md found. Run /fd-map-codebase then /fd-new-feature to start a feature.";
8936
8483
  }
8937
8484
  return "Plan not confirmed. Run /fd-plan and confirm to enable execution.";
8938
8485
  }
8939
8486
  function getBlockMessage(planningDir2) {
8940
- if (!existsSync22(join22(planningDir2, STATE_FILE2))) {
8487
+ if (!existsSync21(join21(planningDir2, STATE_FILE2))) {
8941
8488
  return "No STATE.md found. Run /fd-map-codebase then /fd-new-feature to start a feature.";
8942
8489
  }
8943
8490
  return "Plan not confirmed. Run /fd-plan and confirm to enable execution.";
8944
8491
  }
8945
8492
 
8946
8493
  // src/services/approval-manager.ts
8947
- import { existsSync as existsSync23, readFileSync as readFileSync23, writeFileSync as writeFileSync14, mkdirSync as mkdirSync13 } from "fs";
8948
- import { join as join23 } from "path";
8494
+ import { existsSync as existsSync22, readFileSync as readFileSync22, writeFileSync as writeFileSync13, mkdirSync as mkdirSync12 } from "fs";
8495
+ import { join as join22 } from "path";
8949
8496
  import { createHash as createHash4 } from "crypto";
8950
- import { randomUUID as randomUUID3 } from "crypto";
8497
+ import { randomUUID } from "crypto";
8951
8498
  var APPROVAL_TTL_MS = 30 * 60 * 1000;
8952
8499
  var SENSITIVE_PATTERNS = [
8953
8500
  /auth/i,
@@ -8984,28 +8531,28 @@ function isSensitivePath(filePath) {
8984
8531
  return SENSITIVE_PATTERNS.some((p) => p.test(filePath));
8985
8532
  }
8986
8533
  function approvalsPath(dir) {
8987
- return join23(codebaseDir(dir), "APPROVALS.json");
8534
+ return join22(codebaseDir(dir), "APPROVALS.json");
8988
8535
  }
8989
8536
  function loadStore(dir) {
8990
8537
  const p = approvalsPath(dir);
8991
- if (!existsSync23(p))
8538
+ if (!existsSync22(p))
8992
8539
  return { requests: [] };
8993
8540
  try {
8994
- return JSON.parse(readFileSync23(p, "utf-8"));
8541
+ return JSON.parse(readFileSync22(p, "utf-8"));
8995
8542
  } catch {
8996
8543
  return { requests: [] };
8997
8544
  }
8998
8545
  }
8999
8546
  function saveStore(dir, store) {
9000
8547
  const cd = codebaseDir(dir);
9001
- if (!existsSync23(cd))
9002
- mkdirSync13(cd, { recursive: true });
9003
- writeFileSync14(approvalsPath(dir), JSON.stringify(store, null, 2), "utf-8");
8548
+ if (!existsSync22(cd))
8549
+ mkdirSync12(cd, { recursive: true });
8550
+ writeFileSync13(approvalsPath(dir), JSON.stringify(store, null, 2), "utf-8");
9004
8551
  }
9005
8552
  function requestApproval(dir, run_id, trigger, reason, options = {}) {
9006
8553
  const store = loadStore(dir);
9007
8554
  const req = {
9008
- id: randomUUID3(),
8555
+ id: randomUUID(),
9009
8556
  run_id,
9010
8557
  session_id: options.session_id ?? "session-0",
9011
8558
  requested_at: new Date().toISOString(),
@@ -9032,16 +8579,16 @@ function computeContentHash(args) {
9032
8579
  const payload = JSON.stringify(args, keys);
9033
8580
  return createHash4("sha256").update(payload).digest("hex").slice(0, 16);
9034
8581
  }
9035
- function requestApprovalForTool(dir, run_id, session_id, agent, tool14, args) {
8582
+ function requestApprovalForTool(dir, run_id, session_id, agent, tool13, args) {
9036
8583
  const filePath = extractTargetPath2(args);
9037
8584
  const isSensitive = filePath ? isSensitivePath(filePath) : false;
9038
- return requestApproval(dir, run_id, tool14, `Approval required for tool "${tool14}"`, {
8585
+ return requestApproval(dir, run_id, tool13, `Approval required for tool "${tool13}"`, {
9039
8586
  file_path: filePath,
9040
8587
  risk_score: isSensitive ? 30 : 50,
9041
8588
  session_id,
9042
8589
  agent,
9043
8590
  content_hash: computeContentHash(args),
9044
- change_description: `Tool "${tool14}" requested on ${filePath || "unknown target"}`
8591
+ change_description: `Tool "${tool13}" requested on ${filePath || "unknown target"}`
9045
8592
  });
9046
8593
  }
9047
8594
  function extractTargetPath2(args) {
@@ -9070,8 +8617,8 @@ function ask(reason, riskFlags = []) {
9070
8617
  };
9071
8618
  }
9072
8619
  function evaluate3(input) {
9073
- const { directory, tool: tool14, args } = input;
9074
- if (!WRITE_TOOLS.has(tool14)) {
8620
+ const { directory, tool: tool13, args } = input;
8621
+ if (!WRITE_TOOLS.has(tool13)) {
9075
8622
  return allow3("Tool does not require approval");
9076
8623
  }
9077
8624
  const filePath = String(args?.path ?? args?.file_path ?? args?.filename ?? args?.filePath ?? "");
@@ -9095,23 +8642,23 @@ function evaluate3(input) {
9095
8642
  }
9096
8643
 
9097
8644
  // src/services/deadlock-detector.ts
9098
- import { existsSync as existsSync25, readFileSync as readFileSync25, appendFileSync as appendFileSync6, mkdirSync as mkdirSync15 } from "fs";
9099
- import { join as join25 } from "path";
9100
- import { randomUUID as randomUUID5 } from "crypto";
8645
+ import { existsSync as existsSync24, readFileSync as readFileSync24, appendFileSync as appendFileSync5, mkdirSync as mkdirSync14 } from "fs";
8646
+ import { join as join24 } from "path";
8647
+ import { randomUUID as randomUUID3 } from "crypto";
9101
8648
 
9102
8649
  // src/services/agent-trace-graph.ts
9103
- import { existsSync as existsSync24, readFileSync as readFileSync24, appendFileSync as appendFileSync5, writeFileSync as writeFileSync15, mkdirSync as mkdirSync14 } from "fs";
9104
- import { join as join24 } from "path";
9105
- import { randomUUID as randomUUID4 } from "crypto";
8650
+ import { existsSync as existsSync23, readFileSync as readFileSync23, appendFileSync as appendFileSync4, writeFileSync as writeFileSync14, mkdirSync as mkdirSync13 } from "fs";
8651
+ import { join as join23 } from "path";
8652
+ import { randomUUID as randomUUID2 } from "crypto";
9106
8653
  function agentSpansPath(dir) {
9107
- return join24(codebaseDir(dir), "AGENT_SPANS.jsonl");
8654
+ return join23(codebaseDir(dir), "AGENT_SPANS.jsonl");
9108
8655
  }
9109
8656
  function loadAllSpans(dir) {
9110
8657
  const p = agentSpansPath(dir);
9111
- if (!existsSync24(p))
8658
+ if (!existsSync23(p))
9112
8659
  return [];
9113
8660
  try {
9114
- return readFileSync24(p, "utf-8").trim().split(`
8661
+ return readFileSync23(p, "utf-8").trim().split(`
9115
8662
  `).filter(Boolean).map((l) => JSON.parse(l));
9116
8663
  } catch {
9117
8664
  return [];
@@ -9120,18 +8667,18 @@ function loadAllSpans(dir) {
9120
8667
  function saveAllSpans(dir, spans) {
9121
8668
  const p = agentSpansPath(dir);
9122
8669
  const cd = codebaseDir(dir);
9123
- if (!existsSync24(cd))
9124
- mkdirSync14(cd, { recursive: true });
9125
- writeFileSync15(p, spans.map((s) => JSON.stringify(s)).join(`
8670
+ if (!existsSync23(cd))
8671
+ mkdirSync13(cd, { recursive: true });
8672
+ writeFileSync14(p, spans.map((s) => JSON.stringify(s)).join(`
9126
8673
  `) + `
9127
8674
  `, "utf-8");
9128
8675
  }
9129
8676
  function openSpan(dir, opts) {
9130
8677
  const cd = codebaseDir(dir);
9131
- if (!existsSync24(cd))
9132
- mkdirSync14(cd, { recursive: true });
8678
+ if (!existsSync23(cd))
8679
+ mkdirSync13(cd, { recursive: true });
9133
8680
  const span = {
9134
- span_id: randomUUID4(),
8681
+ span_id: randomUUID2(),
9135
8682
  trace_id: opts.trace_id,
9136
8683
  parent_span_id: opts.parent_span_id,
9137
8684
  invoker: opts.invoker,
@@ -9147,7 +8694,7 @@ function openSpan(dir, opts) {
9147
8694
  depth: opts.depth ?? 0,
9148
8695
  model: opts.model
9149
8696
  };
9150
- appendFileSync5(agentSpansPath(dir), JSON.stringify(span) + `
8697
+ appendFileSync4(agentSpansPath(dir), JSON.stringify(span) + `
9151
8698
  `, "utf-8");
9152
8699
  return span;
9153
8700
  }
@@ -9192,9 +8739,6 @@ function addSpanViolation(dir, span_id, violation) {
9192
8739
  function recordContractViolation(dir, span_id, violation) {
9193
8740
  addSpanViolation(dir, span_id, violation);
9194
8741
  }
9195
- function getSpan(dir, span_id) {
9196
- return loadAllSpans(dir).findLast((s) => s.span_id === span_id) ?? null;
9197
- }
9198
8742
  function getTraceSpans(dir, trace_id) {
9199
8743
  return loadAllSpans(dir).filter((s) => s.trace_id === trace_id);
9200
8744
  }
@@ -9216,21 +8760,21 @@ function resolveConfig(directory) {
9216
8760
  }
9217
8761
  }
9218
8762
  function deadlockSignalsPath(dir) {
9219
- return join25(codebaseDir(dir), "DEADLOCK_SIGNALS.jsonl");
8763
+ return join24(codebaseDir(dir), "DEADLOCK_SIGNALS.jsonl");
9220
8764
  }
9221
8765
  function appendSignal(dir, signal) {
9222
8766
  const cd = codebaseDir(dir);
9223
- if (!existsSync25(cd))
9224
- mkdirSync15(cd, { recursive: true });
9225
- appendFileSync6(deadlockSignalsPath(dir), JSON.stringify(signal) + `
8767
+ if (!existsSync24(cd))
8768
+ mkdirSync14(cd, { recursive: true });
8769
+ appendFileSync5(deadlockSignalsPath(dir), JSON.stringify(signal) + `
9226
8770
  `, "utf-8");
9227
8771
  }
9228
8772
  function getSignals(dir, trace_id) {
9229
8773
  const p = deadlockSignalsPath(dir);
9230
- if (!existsSync25(p))
8774
+ if (!existsSync24(p))
9231
8775
  return [];
9232
8776
  try {
9233
- const all = readFileSync25(p, "utf-8").trim().split(`
8777
+ const all = readFileSync24(p, "utf-8").trim().split(`
9234
8778
  `).filter(Boolean).map((l) => JSON.parse(l));
9235
8779
  return trace_id ? all.filter((s) => s.trace_id === trace_id) : all;
9236
8780
  } catch {
@@ -9248,7 +8792,7 @@ function detectAgentBounce(dir, trace_id, cfg) {
9248
8792
  if (count >= cfg.bounceThreshold) {
9249
8793
  const [a, b] = pair.split("→");
9250
8794
  return {
9251
- signal_id: randomUUID5(),
8795
+ signal_id: randomUUID3(),
9252
8796
  trace_id,
9253
8797
  detected_at: new Date().toISOString(),
9254
8798
  type: "agent_bounce",
@@ -9290,7 +8834,7 @@ function detectCircularDelegation(dir, trace_id, cfg) {
9290
8834
  const cycle = findCycle(node, visited, [node]);
9291
8835
  if (cycle) {
9292
8836
  return {
9293
- signal_id: randomUUID5(),
8837
+ signal_id: randomUUID3(),
9294
8838
  trace_id,
9295
8839
  detected_at: new Date().toISOString(),
9296
8840
  type: "circular_delegation",
@@ -9318,7 +8862,7 @@ function detectStepRetryLoop(dir, trace_id, cfg) {
9318
8862
  for (const [key, count] of Object.entries(stageCounts)) {
9319
8863
  if (count >= cfg.retryLoopThreshold) {
9320
8864
  return {
9321
- signal_id: randomUUID5(),
8865
+ signal_id: randomUUID3(),
9322
8866
  trace_id,
9323
8867
  detected_at: new Date().toISOString(),
9324
8868
  type: "step_retry_loop",
@@ -9340,7 +8884,7 @@ function detectStageStall(dir, trace_id, cfg) {
9340
8884
  const elapsed = (now - new Date(span.started_at).getTime()) / 1000 / 60;
9341
8885
  if (elapsed >= cfg.stageStallMinutes) {
9342
8886
  return {
9343
- signal_id: randomUUID5(),
8887
+ signal_id: randomUUID3(),
9344
8888
  trace_id,
9345
8889
  detected_at: new Date().toISOString(),
9346
8890
  type: "stage_stall",
@@ -9377,29 +8921,29 @@ function isTraceStuck(dir, trace_id) {
9377
8921
 
9378
8922
  // src/services/audit-log.ts
9379
8923
  import {
9380
- existsSync as existsSync26,
9381
- mkdirSync as mkdirSync16,
9382
- appendFileSync as appendFileSync7,
9383
- readFileSync as readFileSync26,
9384
- writeFileSync as writeFileSync16,
8924
+ existsSync as existsSync25,
8925
+ mkdirSync as mkdirSync15,
8926
+ appendFileSync as appendFileSync6,
8927
+ readFileSync as readFileSync25,
8928
+ writeFileSync as writeFileSync15,
9385
8929
  renameSync,
9386
8930
  unlinkSync
9387
8931
  } from "fs";
9388
- import { join as join26 } from "path";
8932
+ import { join as join25 } from "path";
9389
8933
  var AUDIT_FILE = "AUDIT.jsonl";
9390
8934
  var ROTATE_LINE_COUNT = 1000;
9391
8935
  function auditPath(directory) {
9392
- return join26(codebaseDir(directory), AUDIT_FILE);
8936
+ return join25(codebaseDir(directory), AUDIT_FILE);
9393
8937
  }
9394
8938
  function ensureDirectory(dir) {
9395
8939
  const base = codebaseDir(dir);
9396
- if (!existsSync26(base)) {
9397
- mkdirSync16(base, { recursive: true });
8940
+ if (!existsSync25(base)) {
8941
+ mkdirSync15(base, { recursive: true });
9398
8942
  }
9399
8943
  }
9400
8944
  function rotateIfNeeded(path, appLog) {
9401
8945
  try {
9402
- const content = readFileSync26(path, "utf-8");
8946
+ const content = readFileSync25(path, "utf-8");
9403
8947
  const lines = content.split(`
9404
8948
  `).filter((line) => line.trim().length > 0);
9405
8949
  if (lines.length <= ROTATE_LINE_COUNT)
@@ -9407,7 +8951,7 @@ function rotateIfNeeded(path, appLog) {
9407
8951
  const backupPath = `${path}.backup`;
9408
8952
  renameSync(path, backupPath);
9409
8953
  const keep = lines.slice(-ROTATE_LINE_COUNT);
9410
- writeFileSync16(path, keep.join(`
8954
+ writeFileSync15(path, keep.join(`
9411
8955
  `) + `
9412
8956
  `, "utf-8");
9413
8957
  try {
@@ -9423,16 +8967,16 @@ function rotateIfNeeded(path, appLog) {
9423
8967
  function appendAuditEntry(directory, entry, appLog) {
9424
8968
  ensureDirectory(directory);
9425
8969
  const path = auditPath(directory);
9426
- appendFileSync7(path, JSON.stringify(entry) + `
8970
+ appendFileSync6(path, JSON.stringify(entry) + `
9427
8971
  `, "utf-8");
9428
8972
  rotateIfNeeded(path, appLog);
9429
8973
  }
9430
8974
  function queryAudit(directory, filter) {
9431
8975
  const path = auditPath(directory);
9432
- if (!existsSync26(path))
8976
+ if (!existsSync25(path))
9433
8977
  return [];
9434
8978
  try {
9435
- const lines = readFileSync26(path, "utf-8").split(`
8979
+ const lines = readFileSync25(path, "utf-8").split(`
9436
8980
  `).filter((line) => line.trim().length > 0);
9437
8981
  const results = [];
9438
8982
  for (const line of lines) {
@@ -9498,7 +9042,7 @@ function validationToDecision(result) {
9498
9042
  }
9499
9043
  function buildAuditEntry(input, decision) {
9500
9044
  return {
9501
- id: randomUUID6(),
9045
+ id: randomUUID4(),
9502
9046
  timestamp: new Date().toISOString(),
9503
9047
  run_id: input.runId,
9504
9048
  session_id: input.sessionID,
@@ -9531,11 +9075,6 @@ function createHarnessPolicy(directory, appLog) {
9531
9075
  }
9532
9076
  }
9533
9077
  }
9534
- function ensureBudget(runId) {
9535
- if (!getBudget(runId)) {
9536
- init(runId, config);
9537
- }
9538
- }
9539
9078
  function checkRuntimeLimits(input) {
9540
9079
  try {
9541
9080
  const loop = loopDetector.checkBefore(input.tool, input.args, input.sessionID);
@@ -9545,11 +9084,6 @@ function createHarnessPolicy(directory, appLog) {
9545
9084
  if (loop.action === "warn") {
9546
9085
  return allow4(loop.message, "loop-detector", ["loop-warning"]);
9547
9086
  }
9548
- ensureBudget(input.runId);
9549
- const spend = checkSpend(input.runId, input.tool);
9550
- if (spend.verdict === "deny") {
9551
- return deny3(spend.reason, "delegation-budget", spend.escalationMessage ?? "Tool call budget exhausted", ["budget-exhausted"]);
9552
- }
9553
9087
  if (isTraceStuck(input.directory, input.runId)) {
9554
9088
  return deny3("Run is stuck (deadlock signal with auto_stop)", "deadlock-detector", "Deadlock detection flagged this run for auto-stop. Escalate to the human.", ["deadlock"]);
9555
9089
  }
@@ -9728,7 +9262,7 @@ function createHarnessPolicy(directory, appLog) {
9728
9262
  }
9729
9263
 
9730
9264
  // src/services/harness-controller.ts
9731
- import { randomUUID as randomUUID8 } from "crypto";
9265
+ import { randomUUID as randomUUID7 } from "crypto";
9732
9266
 
9733
9267
  // src/services/preflight-explorer.ts
9734
9268
  var QUESTION_KIND_PATTERNS = [
@@ -10242,10 +9776,86 @@ function classifyTaskWithContext(description, exploration, sessionHistory = [])
10242
9776
  };
10243
9777
  }
10244
9778
 
9779
+ // src/services/run-trace.ts
9780
+ import { existsSync as existsSync26, readFileSync as readFileSync26, appendFileSync as appendFileSync7, writeFileSync as writeFileSync16, mkdirSync as mkdirSync16 } from "fs";
9781
+ import { join as join26 } from "path";
9782
+ import { randomUUID as randomUUID5 } from "crypto";
9783
+ function runsPath(dir) {
9784
+ return join26(codebaseDir(dir), "RUNS.jsonl");
9785
+ }
9786
+ function startTrace(dir, command, args, session_id = "session-0") {
9787
+ const cd = codebaseDir(dir);
9788
+ if (!existsSync26(cd))
9789
+ mkdirSync16(cd, { recursive: true });
9790
+ const trace = {
9791
+ run_id: randomUUID5(),
9792
+ session_id,
9793
+ command,
9794
+ args,
9795
+ started_at: new Date().toISOString(),
9796
+ status: "running",
9797
+ files_touched: [],
9798
+ event_ids: [],
9799
+ risk_score: 0
9800
+ };
9801
+ appendFileSync7(runsPath(dir), JSON.stringify(trace) + `
9802
+ `, "utf-8");
9803
+ return trace;
9804
+ }
9805
+ function loadAllTraces(dir) {
9806
+ const p = runsPath(dir);
9807
+ if (!existsSync26(p))
9808
+ return [];
9809
+ try {
9810
+ return readFileSync26(p, "utf-8").trim().split(`
9811
+ `).filter(Boolean).map((l) => JSON.parse(l));
9812
+ } catch {
9813
+ return [];
9814
+ }
9815
+ }
9816
+ function saveAllTraces(dir, traces) {
9817
+ const p = runsPath(dir);
9818
+ writeFileSync16(p, traces.map((t) => JSON.stringify(t)).join(`
9819
+ `) + `
9820
+ `, "utf-8");
9821
+ }
9822
+ function endTrace(dir, run_id, status, outcome, error) {
9823
+ const traces = loadAllTraces(dir);
9824
+ const idx = traces.findLastIndex((t) => t.run_id === run_id);
9825
+ if (idx === -1)
9826
+ return;
9827
+ traces[idx] = {
9828
+ ...traces[idx],
9829
+ ended_at: new Date().toISOString(),
9830
+ status,
9831
+ ...outcome ? { outcome } : {},
9832
+ ...error ? { error } : {}
9833
+ };
9834
+ saveAllTraces(dir, traces);
9835
+ }
9836
+ function getTrace(dir, run_id) {
9837
+ return loadAllTraces(dir).findLast((t) => t.run_id === run_id) ?? null;
9838
+ }
9839
+ function recordVerification(dir, run_id, kind, evidence) {
9840
+ const traces = loadAllTraces(dir);
9841
+ const idx = traces.findLastIndex((t) => t.run_id === run_id);
9842
+ if (idx === -1)
9843
+ return;
9844
+ const list = traces[idx].verifications ?? [];
9845
+ traces[idx] = {
9846
+ ...traces[idx],
9847
+ verifications: [
9848
+ ...list,
9849
+ { kind, evidence, timestamp: new Date().toISOString() }
9850
+ ]
9851
+ };
9852
+ saveAllTraces(dir, traces);
9853
+ }
9854
+
10245
9855
  // src/services/workflow-scorecard.ts
10246
9856
  import { existsSync as existsSync27, readFileSync as readFileSync27, appendFileSync as appendFileSync8, mkdirSync as mkdirSync17 } from "fs";
10247
9857
  import { join as join27 } from "path";
10248
- import { randomUUID as randomUUID7 } from "crypto";
9858
+ import { randomUUID as randomUUID6 } from "crypto";
10249
9859
  var DIMENSION_WEIGHTS = {
10250
9860
  stageCompliance: 0.07,
10251
9861
  designFirstCompliance: 0.1,
@@ -10310,7 +9920,7 @@ function generateScorecard(dir, trace, input = {}) {
10310
9920
  const overallScore = Math.round(Object.entries(dimensions).reduce((sum, [key, val]) => sum + val * DIMENSION_WEIGHTS[key] * 100, 0));
10311
9921
  const completion_status = trace.status === "complete" ? "complete" : trace.status === "failed" ? "failed" : trace.status === "cancelled" ? "cancelled" : "blocked";
10312
9922
  const scorecard = {
10313
- scorecard_id: randomUUID7(),
9923
+ scorecard_id: randomUUID6(),
10314
9924
  run_id: trace.run_id,
10315
9925
  session_id: trace.session_id,
10316
9926
  command: trace.command,
@@ -10363,7 +9973,9 @@ function createStatePersistence(options) {
10363
9973
  command,
10364
9974
  rootSpanId: span.span_id,
10365
9975
  currentSpanId: span.span_id,
10366
- status: "running"
9976
+ status: "running",
9977
+ toolCallCount: 0,
9978
+ sameStepRetryCount: 0
10367
9979
  };
10368
9980
  contexts.set(sessionID, ctx);
10369
9981
  return ctx;
@@ -10386,25 +9998,6 @@ function createStatePersistence(options) {
10386
9998
  }
10387
9999
  }
10388
10000
  },
10389
- openAgentSpan(ctx, parentSpanId, agent, task, stage3) {
10390
- const parent = parentSpanId ? getSpan(directory, parentSpanId) : undefined;
10391
- const depth = parent ? parent.depth + 1 : 0;
10392
- const span = openSpan(directory, {
10393
- trace_id: ctx.runId,
10394
- invoker: ctx.command,
10395
- agent,
10396
- task_description: task,
10397
- stage: stage3,
10398
- parent_span_id: parentSpanId,
10399
- depth
10400
- });
10401
- const updated = { ...ctx, currentSpanId: span.span_id };
10402
- contexts.set(ctx.sessionID, updated);
10403
- return span;
10404
- },
10405
- closeAgentSpan(spanId, status, opts = {}) {
10406
- closeSpan(directory, spanId, status === "running" ? "failed" : status, opts);
10407
- },
10408
10001
  recordObservation(ctx, type, content) {
10409
10002
  const list = observations.get(ctx.runId) ?? [];
10410
10003
  list.push({
@@ -10425,8 +10018,8 @@ function createStatePersistence(options) {
10425
10018
 
10426
10019
  // src/services/execution-substrate.ts
10427
10020
  var executions = new Map;
10428
- function makeKey(runId, sessionID, tool14) {
10429
- return `${runId}:${sessionID}:${tool14}`;
10021
+ function makeKey(runId, sessionID, tool13) {
10022
+ return `${runId}:${sessionID}:${tool13}`;
10430
10023
  }
10431
10024
  function createExecutionSubstrate() {
10432
10025
  return {
@@ -10894,7 +10487,7 @@ function blockMessage(toolName) {
10894
10487
 
10895
10488
  ` + `Allowed tools for orchestrator: ${allowed.join(", ")}.
10896
10489
 
10897
- ` + `To delegate execution, use the \`delegate\` tool.
10490
+ ` + `To route execution, mention the agent directly: @default-executor, @backend-coder, etc.
10898
10491
 
10899
10492
  ` + `To disable this guard: set FLOWDECK_ORCHESTRATOR_GUARD=off`;
10900
10493
  }
@@ -11096,6 +10689,7 @@ function createVerificationLayer(directory) {
11096
10689
 
11097
10690
  // src/services/recovery-layer.ts
11098
10691
  var AUTO_STOP_TYPES = ["circular_delegation"];
10692
+ var retryCounts = new Map;
11099
10693
  function signalToRecoveryAction(signal) {
11100
10694
  switch (signal.recommended_action) {
11101
10695
  case "stop":
@@ -11109,10 +10703,10 @@ function signalToRecoveryAction(signal) {
11109
10703
  return "retry";
11110
10704
  }
11111
10705
  }
11112
- function formatBudget(budget) {
11113
- if (!budget)
11114
- return "budget unavailable";
11115
- return `tool_calls=${budget.spentToolCalls}/${budget.maxToolCalls}, depth=${budget.currentDepth}/${budget.maxDepth}, retries=${budget.sameStepRetries}/${budget.maxSameStepRetries}`;
10706
+ function formatBudget(ctx) {
10707
+ const toolCalls = ctx.toolCallCount ?? 0;
10708
+ const retries = ctx.sameStepRetryCount ?? 0;
10709
+ return `tool_calls=${toolCalls}, retries=${retries}`;
11116
10710
  }
11117
10711
  function taskTypeToFailureTag(taskType) {
11118
10712
  switch (taskType) {
@@ -11143,11 +10737,10 @@ function createRecoveryLayer(appLog) {
11143
10737
  return isTraceStuck(dir, runId);
11144
10738
  },
11145
10739
  explainBlockage(ctx) {
11146
- const budget = getBudget(ctx.runId);
11147
10740
  const signals = detectDeadlocks(ctx.directory, ctx.runId);
11148
10741
  const lines = [];
11149
10742
  lines.push(`Run ${ctx.runId} (${ctx.command}) is blocked.`);
11150
- lines.push(`Budget state: ${formatBudget(budget)}`);
10743
+ lines.push(`Budget state: ${formatBudget(ctx)}`);
11151
10744
  if (signals.length === 0) {
11152
10745
  lines.push("No deadlock signals detected.");
11153
10746
  } else {
@@ -11160,14 +10753,11 @@ function createRecoveryLayer(appLog) {
11160
10753
  `);
11161
10754
  },
11162
10755
  recommendRecovery(ctx, signals) {
11163
- const budget = getBudget(ctx.runId);
11164
10756
  if (signals.some((s) => s.auto_stop || AUTO_STOP_TYPES.includes(s.type))) {
11165
10757
  return "stop";
11166
10758
  }
11167
- if (budget && budget.remainingToolCalls <= 0) {
11168
- return "escalate";
11169
- }
11170
- if (budget && budget.sameStepRetries >= budget.maxSameStepRetries) {
10759
+ const maxRetries = 3;
10760
+ if ((ctx.sameStepRetryCount ?? 0) >= maxRetries) {
11171
10761
  return "escalate";
11172
10762
  }
11173
10763
  const severityOrder = ["stop", "escalate", "fallback-agent", "retry"];
@@ -11179,7 +10769,9 @@ function createRecoveryLayer(appLog) {
11179
10769
  return "retry";
11180
10770
  },
11181
10771
  canRetry(runId) {
11182
- return incrementSameStepRetry(runId);
10772
+ const current = retryCounts.get(runId) ?? 0;
10773
+ retryCounts.set(runId, current + 1);
10774
+ return current < 3;
11183
10775
  },
11184
10776
  getRelevantFailures(dir, taskType) {
11185
10777
  try {
@@ -11260,8 +10852,8 @@ function createToolOutcomeHandler(deps) {
11260
10852
  executionSubstrate,
11261
10853
  state
11262
10854
  } = deps;
11263
- function getLastAuditEntryId(runId, tool14) {
11264
- const entries = auditLog.query({ run_id: runId, tool: tool14 });
10855
+ function getLastAuditEntryId(runId, tool13) {
10856
+ const entries = auditLog.query({ run_id: runId, tool: tool13 });
11265
10857
  return entries.at(-1)?.id;
11266
10858
  }
11267
10859
  return (req, output, status) => {
@@ -11409,8 +11001,8 @@ function createHarnessController(config) {
11409
11001
  const orchestratorGuard = new OrchestratorGuard;
11410
11002
  orchestratorGuard.setPolicy(policy);
11411
11003
  const loopDetector = lazy(() => {
11412
- const flowdeckConfig = loadFlowDeckConfig(directory);
11413
- const loopCfg = flowdeckConfig.governance?.loopDetection ?? {};
11004
+ const flowdeckConfig2 = loadFlowDeckConfig(directory);
11005
+ const loopCfg = flowdeckConfig2.governance?.loopDetection ?? {};
11414
11006
  return new LoopDetector({
11415
11007
  enabled: loopCfg.enabled ?? true,
11416
11008
  maxRepeats: loopCfg.maxRepeats ?? 2,
@@ -11424,6 +11016,12 @@ function createHarnessController(config) {
11424
11016
  const contextMonitor = lazy(() => config.contextMonitor ?? createContextWindowMonitorHook({
11425
11017
  getTotalBudget: (sessionID) => contextIngress.getTotalBudget(sessionID)
11426
11018
  }));
11019
+ const flowdeckConfig = loadFlowDeckConfig(directory);
11020
+ const delegationConfig = flowdeckConfig.governance?.delegationBudget;
11021
+ const maxDepth = delegationConfig?.maxDepth ?? 3;
11022
+ const maxToolCalls = delegationConfig?.maxToolCalls ?? 200;
11023
+ const sessionDepthMap = new Map;
11024
+ const blockedSessions = new Set;
11427
11025
  const state = {
11428
11026
  loopDetector,
11429
11027
  eventLog,
@@ -11480,7 +11078,7 @@ function createHarnessController(config) {
11480
11078
  };
11481
11079
  function buildCommandAuditEntry(req, runCtx) {
11482
11080
  return {
11483
- id: randomUUID8(),
11081
+ id: randomUUID7(),
11484
11082
  timestamp: new Date().toISOString(),
11485
11083
  run_id: runCtx.runId,
11486
11084
  session_id: req.sessionID,
@@ -11601,10 +11199,52 @@ function createHarnessController(config) {
11601
11199
  recordToolOutcome(req, output, status) {
11602
11200
  toolOutcomeHandler(req, output, status);
11603
11201
  },
11202
+ checkToolCallBudget(sessionID) {
11203
+ if (blockedSessions.has(sessionID)) {
11204
+ return {
11205
+ allow: false,
11206
+ reason: `Session ${sessionID} blocked: delegation depth limit exceeded. Agent must surface result to user rather than delegating further.`
11207
+ };
11208
+ }
11209
+ const runCtx = statePersistence.getRunContext(sessionID);
11210
+ if (runCtx) {
11211
+ const nextCount = (runCtx.toolCallCount ?? 0) + 1;
11212
+ if (nextCount > maxToolCalls) {
11213
+ return {
11214
+ allow: false,
11215
+ reason: `Run ${runCtx.runId} exceeded maxToolCalls (${maxToolCalls}). Stop and summarize progress to the user.`
11216
+ };
11217
+ }
11218
+ statePersistence.updateRunContext({ ...runCtx, toolCallCount: nextCount });
11219
+ }
11220
+ return { allow: true };
11221
+ },
11604
11222
  async onSessionEvent(evt) {
11605
11223
  const type = evt.type;
11606
11224
  const properties = evt.properties ?? {};
11607
- const sessionID = String(properties.sessionID ?? properties.id ?? properties.sessionId ?? "");
11225
+ const sessionID = extractSessionId2(properties) ?? "";
11226
+ const parentSessionID = extractParentSessionId2(properties);
11227
+ if (type === "session.created" && sessionID) {
11228
+ if (parentSessionID) {
11229
+ const parentCtx = statePersistence.getRunContext(parentSessionID);
11230
+ if (parentCtx) {
11231
+ statePersistence.updateRunContext({ ...parentCtx, sessionID });
11232
+ }
11233
+ const parentDepth = sessionDepthMap.get(parentSessionID) ?? 0;
11234
+ const childDepth = parentDepth + 1;
11235
+ sessionDepthMap.set(sessionID, childDepth);
11236
+ if (childDepth > maxDepth) {
11237
+ log(`[harness] delegation depth ${childDepth} exceeds maxDepth ${maxDepth} — session ${sessionID} will be policy-blocked`);
11238
+ blockedSessions.add(sessionID);
11239
+ }
11240
+ } else {
11241
+ sessionDepthMap.set(sessionID, 0);
11242
+ }
11243
+ }
11244
+ if ((type === "session.idle" || type === "session.error") && sessionID) {
11245
+ sessionDepthMap.delete(sessionID);
11246
+ blockedSessions.delete(sessionID);
11247
+ }
11608
11248
  orchestratorGuard.onEvent({ type, properties });
11609
11249
  if (type === "session.created" || type === "session.started") {
11610
11250
  const healthy = await eventLog.get().session({ directory }, { type, properties });
@@ -11651,6 +11291,30 @@ function createHarnessController(config) {
11651
11291
  }
11652
11292
  };
11653
11293
  }
11294
+ function extractSessionId2(props) {
11295
+ const id = props.sessionID ?? props.sessionId ?? props.id;
11296
+ if (typeof id === "string")
11297
+ return id;
11298
+ const info = props.info;
11299
+ if (typeof info?.sessionID === "string")
11300
+ return info.sessionID;
11301
+ if (typeof info?.sessionId === "string")
11302
+ return info.sessionId;
11303
+ if (typeof info?.id === "string")
11304
+ return info.id;
11305
+ return;
11306
+ }
11307
+ function extractParentSessionId2(props) {
11308
+ const parentId = props.parentID ?? props.parentId ?? props.parentSessionId ?? props.parent_session_id;
11309
+ if (typeof parentId === "string")
11310
+ return parentId;
11311
+ const info = props.info;
11312
+ if (typeof info?.parentID === "string")
11313
+ return info.parentID;
11314
+ if (typeof info?.parentId === "string")
11315
+ return info.parentId;
11316
+ return;
11317
+ }
11654
11318
 
11655
11319
  // src/index.ts
11656
11320
  function lazyLoadRulePaths(projectRoot) {
@@ -11710,7 +11374,6 @@ var plugin = async (input, _options) => {
11710
11374
  contextIngress
11711
11375
  });
11712
11376
  harness.init();
11713
- const delegateTool = createDelegateTool(client, harness.getStatePersistence(), (input2) => harness.ensureRunContext(input2));
11714
11377
  const contextMonitor = harness.getContextMonitor();
11715
11378
  const shellEnvHook = createShellEnvHook({ directory, worktree });
11716
11379
  const todoHook = createTodoHook(client);
@@ -11816,8 +11479,7 @@ var plugin = async (input, _options) => {
11816
11479
  codegraph: codegraphTool,
11817
11480
  "load-rules": loadRulesTool,
11818
11481
  "list-rules": listRulesTool,
11819
- "merge-assist": mergeAssistTool,
11820
- delegate: delegateTool
11482
+ "merge-assist": mergeAssistTool
11821
11483
  },
11822
11484
  "shell.env": shellEnvHook,
11823
11485
  "todo.updated": todoHook,
@@ -11870,6 +11532,11 @@ var plugin = async (input, _options) => {
11870
11532
  }
11871
11533
  },
11872
11534
  "tool.execute.before": async (toolInput, toolOutput) => {
11535
+ const sessionID = toolInput.sessionID ?? "";
11536
+ const budgetCheck = harness.checkToolCallBudget(sessionID);
11537
+ if (!budgetCheck.allow) {
11538
+ throw new Error(budgetCheck.reason ?? `Tool blocked: ${budgetCheck.reason}`);
11539
+ }
11873
11540
  if ((toolInput.tool === "read" || toolInput.tool === "view") && toolOutput?.args) {
11874
11541
  if (typeof toolOutput.args.offset === "string") {
11875
11542
  const n = Number(toolOutput.args.offset);
@@ -11881,7 +11548,7 @@ var plugin = async (input, _options) => {
11881
11548
  }
11882
11549
  }
11883
11550
  const decision = harness.evaluateToolCall({
11884
- sessionID: toolInput.sessionID ?? "",
11551
+ sessionID,
11885
11552
  tool: toolInput.tool ?? toolInput.name ?? "unknown",
11886
11553
  args: toolOutput?.args ?? toolInput?.args ?? {},
11887
11554
  agent: toolInput.agent