@dv.nghiem/flowdeck 0.5.3 → 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/README.md +7 -5
- package/dist/agents/orchestrator.d.ts.map +1 -1
- package/dist/hooks/event-log-hook.d.ts +2 -2
- package/dist/hooks/event-log-hook.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +377 -808
- package/dist/services/agent-trace-graph.d.ts +2 -2
- package/dist/services/agent-trace-graph.d.ts.map +1 -1
- package/dist/services/context-ingress.d.ts.map +1 -1
- package/dist/services/harness-controller.d.ts +5 -1
- package/dist/services/harness-controller.d.ts.map +1 -1
- package/dist/services/harness-policy.d.ts.map +1 -1
- package/dist/services/harness-types.d.ts +6 -33
- package/dist/services/harness-types.d.ts.map +1 -1
- package/dist/services/recovery-layer.d.ts.map +1 -1
- package/dist/services/run-trace.d.ts +0 -3
- package/dist/services/run-trace.d.ts.map +1 -1
- package/dist/services/state-persistence.d.ts +1 -3
- package/dist/services/state-persistence.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/services/delegation-budget.d.ts +0 -56
- package/dist/services/delegation-budget.d.ts.map +0 -1
- package/dist/tools/delegate.d.ts +0 -16
- package/dist/tools/delegate.d.ts.map +0 -1
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
|
|
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
|
|
1288
|
+
### Direct Execution Path
|
|
1290
1289
|
When workflow class is \`quick\` or \`docs-only\` (simple):
|
|
1291
|
-
-
|
|
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
|
-
-
|
|
1302
|
-
-
|
|
1303
|
-
-
|
|
1304
|
-
-
|
|
1305
|
-
-
|
|
1306
|
-
-
|
|
1307
|
-
-
|
|
1308
|
-
-
|
|
1309
|
-
-
|
|
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
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
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
|
-
|
|
1320
|
-
|
|
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,567 +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 existing = budgets.get(runId);
|
|
5861
|
-
if (existing)
|
|
5862
|
-
return existing;
|
|
5863
|
-
const resolved = resolveDelegationBudgetConfig(config);
|
|
5864
|
-
const budget = {
|
|
5865
|
-
runId,
|
|
5866
|
-
config: resolved,
|
|
5867
|
-
spentToolCalls: 0,
|
|
5868
|
-
currentDepth: 0,
|
|
5869
|
-
sameStepRetries: 0
|
|
5870
|
-
};
|
|
5871
|
-
budgets.set(runId, budget);
|
|
5872
|
-
return budget;
|
|
5873
|
-
}
|
|
5874
|
-
function getBudget(runId) {
|
|
5875
|
-
const budget = budgets.get(runId);
|
|
5876
|
-
if (!budget)
|
|
5877
|
-
return null;
|
|
5878
|
-
return toSnapshot(budget);
|
|
5879
|
-
}
|
|
5880
|
-
function checkSpend(runId, _toolName) {
|
|
5881
|
-
const budget = budgets.get(runId);
|
|
5882
|
-
if (!budget) {
|
|
5883
|
-
return denyDecision("No budget initialized for this run");
|
|
5884
|
-
}
|
|
5885
|
-
const remaining = budget.config.maxToolCalls - budget.spentToolCalls;
|
|
5886
|
-
if (remaining <= 0) {
|
|
5887
|
-
return denyDecision(`Tool call budget exhausted (${budget.spentToolCalls}/${budget.config.maxToolCalls})`);
|
|
5888
|
-
}
|
|
5889
|
-
budget.spentToolCalls += 1;
|
|
5890
|
-
return allowDecision(Math.max(0, budget.config.maxToolCalls - budget.spentToolCalls));
|
|
5891
|
-
}
|
|
5892
|
-
function recordDelegation(parentRunId, childRunId) {
|
|
5893
|
-
const parent = budgets.get(parentRunId);
|
|
5894
|
-
if (!parent)
|
|
5895
|
-
return false;
|
|
5896
|
-
const child = budgets.get(childRunId) ?? init(childRunId);
|
|
5897
|
-
child.config = parent.config;
|
|
5898
|
-
child.currentDepth = parent.currentDepth + 1;
|
|
5899
|
-
budgets.set(childRunId, child);
|
|
5900
|
-
return child.currentDepth <= child.config.maxDepth;
|
|
5901
|
-
}
|
|
5902
|
-
function releaseDelegation(parentRunId, childRunId) {
|
|
5903
|
-
const child = budgets.get(childRunId);
|
|
5904
|
-
if (!child)
|
|
5905
|
-
return false;
|
|
5906
|
-
const parent = budgets.get(parentRunId);
|
|
5907
|
-
child.currentDepth = parent?.currentDepth ?? 0;
|
|
5908
|
-
budgets.set(childRunId, child);
|
|
5909
|
-
return true;
|
|
5910
|
-
}
|
|
5911
|
-
function incrementSameStepRetry(runId) {
|
|
5912
|
-
const budget = budgets.get(runId);
|
|
5913
|
-
if (!budget)
|
|
5914
|
-
return false;
|
|
5915
|
-
budget.sameStepRetries += 1;
|
|
5916
|
-
return budget.sameStepRetries <= budget.config.maxSameStepRetries;
|
|
5917
|
-
}
|
|
5918
|
-
function toSnapshot(budget) {
|
|
5919
|
-
return {
|
|
5920
|
-
runId: budget.runId,
|
|
5921
|
-
maxToolCalls: budget.config.maxToolCalls,
|
|
5922
|
-
maxDepth: budget.config.maxDepth,
|
|
5923
|
-
maxSameStepRetries: budget.config.maxSameStepRetries,
|
|
5924
|
-
spentToolCalls: budget.spentToolCalls,
|
|
5925
|
-
currentDepth: budget.currentDepth,
|
|
5926
|
-
sameStepRetries: budget.sameStepRetries,
|
|
5927
|
-
remainingToolCalls: Math.max(0, budget.config.maxToolCalls - budget.spentToolCalls)
|
|
5928
|
-
};
|
|
5929
|
-
}
|
|
5930
|
-
|
|
5931
|
-
// src/tools/delegate.ts
|
|
5932
|
-
var DEFAULT_TIMEOUT_MS = 120000;
|
|
5933
|
-
var MAX_TIMEOUT_MS = 600000;
|
|
5934
|
-
function normalizeMode(mode) {
|
|
5935
|
-
if (mode === "council" || mode === "pipeline")
|
|
5936
|
-
return mode;
|
|
5937
|
-
if (typeof mode === "string" && mode.length > 0)
|
|
5938
|
-
return mode;
|
|
5939
|
-
return "direct";
|
|
5940
|
-
}
|
|
5941
|
-
function validateCouncilContext(context) {
|
|
5942
|
-
if (!context || !Array.isArray(context.agents) || context.agents.length === 0) {
|
|
5943
|
-
return { ok: false, error: "council mode requires context.agents to be a non-empty array of strings." };
|
|
5944
|
-
}
|
|
5945
|
-
for (const agent of context.agents) {
|
|
5946
|
-
if (typeof agent !== "string") {
|
|
5947
|
-
return { ok: false, error: "context.agents must contain only strings." };
|
|
5948
|
-
}
|
|
5949
|
-
}
|
|
5950
|
-
return { ok: true, agents: context.agents };
|
|
5951
|
-
}
|
|
5952
|
-
function validatePipelineContext(context) {
|
|
5953
|
-
if (!context || !Array.isArray(context.stages) || context.stages.length === 0) {
|
|
5954
|
-
return { ok: false, error: "pipeline mode requires context.stages to be a non-empty array of { agent, task? }." };
|
|
5955
|
-
}
|
|
5956
|
-
for (const stage of context.stages) {
|
|
5957
|
-
if (!stage || typeof stage !== "object" || typeof stage.agent !== "string") {
|
|
5958
|
-
return { ok: false, error: "Each pipeline stage must be an object with a string 'agent' property." };
|
|
5959
|
-
}
|
|
5960
|
-
}
|
|
5961
|
-
return { ok: true, stages: context.stages };
|
|
5962
|
-
}
|
|
5963
|
-
function isRegisteredAgent(agent) {
|
|
5964
|
-
return AGENT_NAMES.includes(agent);
|
|
5965
|
-
}
|
|
5966
|
-
function createDelegateTool(client, statePersistence, ensureRunContext) {
|
|
5967
|
-
return tool13({
|
|
5968
|
-
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? }).",
|
|
5969
|
-
args: {
|
|
5970
|
-
agent: tool13.schema.string(),
|
|
5971
|
-
task: tool13.schema.string(),
|
|
5972
|
-
mode: tool13.schema.enum(["direct", "council", "pipeline"]).optional().default("direct"),
|
|
5973
|
-
context: tool13.schema.object().optional()
|
|
5974
|
-
},
|
|
5975
|
-
async execute(args, ctx) {
|
|
5976
|
-
const { agent, task, mode, context } = args;
|
|
5977
|
-
const directory = ctx.directory;
|
|
5978
|
-
const sessionID = ctx.sessionID;
|
|
5979
|
-
const resolvedMode = normalizeMode(mode);
|
|
5980
|
-
let runCtx = statePersistence.getRunContext(sessionID);
|
|
5981
|
-
if (!runCtx) {
|
|
5982
|
-
runCtx = ensureRunContext({
|
|
5983
|
-
sessionID,
|
|
5984
|
-
description: task,
|
|
5985
|
-
agent
|
|
5986
|
-
});
|
|
5987
|
-
}
|
|
5988
|
-
if (!getBudget(runCtx.runId)) {
|
|
5989
|
-
init(runCtx.runId);
|
|
5990
|
-
}
|
|
5991
|
-
const childRunId = randomUUID2();
|
|
5992
|
-
const withinDepth = recordDelegation(runCtx.runId, childRunId);
|
|
5993
|
-
if (!withinDepth) {
|
|
5994
|
-
const snapshot = getBudget(runCtx.runId);
|
|
5995
|
-
return {
|
|
5996
|
-
span_id: "",
|
|
5997
|
-
run_id: runCtx.runId,
|
|
5998
|
-
status: "blocked",
|
|
5999
|
-
output: "",
|
|
6000
|
-
error: `Delegation depth limit exceeded (current: ${snapshot?.currentDepth ?? 0}, max: ${snapshot?.maxDepth ?? 0}). Escalate to the human.`
|
|
6001
|
-
};
|
|
6002
|
-
}
|
|
6003
|
-
try {
|
|
6004
|
-
if (resolvedMode === "direct") {
|
|
6005
|
-
if (!isRegisteredAgent(agent)) {
|
|
6006
|
-
return {
|
|
6007
|
-
span_id: "",
|
|
6008
|
-
run_id: runCtx.runId,
|
|
6009
|
-
status: "blocked",
|
|
6010
|
-
output: "",
|
|
6011
|
-
error: `Agent "${agent}" is not a registered FlowDeck agent. Registered agents: ${AGENT_NAMES.join(", ")}.`
|
|
6012
|
-
};
|
|
6013
|
-
}
|
|
6014
|
-
const span = statePersistence.openAgentSpan(runCtx, runCtx.currentSpanId, agent, task, runCtx.currentStage ?? "execute");
|
|
6015
|
-
let result;
|
|
6016
|
-
try {
|
|
6017
|
-
result = await executeDirectDelegation(client, ctx, agent, task, span, runCtx, statePersistence, context);
|
|
6018
|
-
} catch (error) {
|
|
6019
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
6020
|
-
result = { status: "failed", output: "", error: message };
|
|
6021
|
-
}
|
|
6022
|
-
const spanStatus = result.status === "blocked" ? "blocked" : result.status === "failed" ? "failed" : "complete";
|
|
6023
|
-
statePersistence.closeAgentSpan(span.span_id, spanStatus, {
|
|
6024
|
-
output_valid: result.status === "complete"
|
|
6025
|
-
});
|
|
6026
|
-
return {
|
|
6027
|
-
span_id: span.span_id,
|
|
6028
|
-
run_id: runCtx.runId,
|
|
6029
|
-
...result
|
|
6030
|
-
};
|
|
6031
|
-
}
|
|
6032
|
-
if (resolvedMode === "council") {
|
|
6033
|
-
const validation = validateCouncilContext(context);
|
|
6034
|
-
if (!validation.ok) {
|
|
6035
|
-
return {
|
|
6036
|
-
span_id: "",
|
|
6037
|
-
run_id: runCtx.runId,
|
|
6038
|
-
status: "blocked",
|
|
6039
|
-
output: "",
|
|
6040
|
-
error: validation.error
|
|
6041
|
-
};
|
|
6042
|
-
}
|
|
6043
|
-
const unknownAgents = validation.agents.filter((a) => !isRegisteredAgent(a));
|
|
6044
|
-
if (unknownAgents.length > 0) {
|
|
6045
|
-
return {
|
|
6046
|
-
span_id: "",
|
|
6047
|
-
run_id: runCtx.runId,
|
|
6048
|
-
status: "blocked",
|
|
6049
|
-
output: "",
|
|
6050
|
-
error: `Agents are not registered FlowDeck agents: ${unknownAgents.join(", ")}. Registered agents: ${AGENT_NAMES.join(", ")}.`
|
|
6051
|
-
};
|
|
6052
|
-
}
|
|
6053
|
-
const results = await runCouncil(client, { directory, sessionID }, task, validation.agents, { maxConcurrency: 3 });
|
|
6054
|
-
const spans = [];
|
|
6055
|
-
for (const r of results) {
|
|
6056
|
-
const span = statePersistence.openAgentSpan(runCtx, runCtx.currentSpanId, r.agent, task, runCtx.currentStage ?? "execute");
|
|
6057
|
-
const status = r.error ? "failed" : "complete";
|
|
6058
|
-
statePersistence.closeAgentSpan(span.span_id, status, { output_valid: status === "complete" });
|
|
6059
|
-
spans.push({ span_id: span.span_id, status });
|
|
6060
|
-
}
|
|
6061
|
-
const anySuccess = results.some((r) => !r.error);
|
|
6062
|
-
const output = results.map((r) => `--- ${r.agent} ---
|
|
6063
|
-
${r.error ? `ERROR: ${r.error}` : r.output}`).join(`
|
|
6064
|
-
|
|
6065
|
-
`);
|
|
6066
|
-
return {
|
|
6067
|
-
span_id: spans[0]?.span_id ?? "",
|
|
6068
|
-
run_id: runCtx.runId,
|
|
6069
|
-
status: anySuccess ? "complete" : results.every((r) => r.error) ? "failed" : "blocked",
|
|
6070
|
-
output
|
|
6071
|
-
};
|
|
6072
|
-
}
|
|
6073
|
-
if (resolvedMode === "pipeline") {
|
|
6074
|
-
const validation = validatePipelineContext(context);
|
|
6075
|
-
if (!validation.ok) {
|
|
6076
|
-
return {
|
|
6077
|
-
span_id: "",
|
|
6078
|
-
run_id: runCtx.runId,
|
|
6079
|
-
status: "blocked",
|
|
6080
|
-
output: "",
|
|
6081
|
-
error: validation.error
|
|
6082
|
-
};
|
|
6083
|
-
}
|
|
6084
|
-
const progress = [];
|
|
6085
|
-
let previousOutput = "";
|
|
6086
|
-
for (const stage of validation.stages) {
|
|
6087
|
-
if (!isRegisteredAgent(stage.agent)) {
|
|
6088
|
-
progress.push({
|
|
6089
|
-
stage: stage.agent,
|
|
6090
|
-
status: "blocked",
|
|
6091
|
-
error: `Agent "${stage.agent}" is not a registered FlowDeck agent.`
|
|
6092
|
-
});
|
|
6093
|
-
return {
|
|
6094
|
-
span_id: "",
|
|
6095
|
-
run_id: runCtx.runId,
|
|
6096
|
-
status: "blocked",
|
|
6097
|
-
output: "",
|
|
6098
|
-
error: `Agent "${stage.agent}" is not a registered FlowDeck agent.`,
|
|
6099
|
-
pipeline_progress: progress
|
|
6100
|
-
};
|
|
6101
|
-
}
|
|
6102
|
-
const span = statePersistence.openAgentSpan(runCtx, runCtx.currentSpanId, stage.agent, stage.task ?? task, runCtx.currentStage ?? "execute");
|
|
6103
|
-
const stageContext = {
|
|
6104
|
-
...context,
|
|
6105
|
-
previousOutput
|
|
6106
|
-
};
|
|
6107
|
-
let result;
|
|
6108
|
-
try {
|
|
6109
|
-
result = await executeDirectDelegation(client, ctx, stage.agent, stage.task ?? task, span, runCtx, statePersistence, stageContext);
|
|
6110
|
-
} catch (error) {
|
|
6111
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
6112
|
-
result = { status: "failed", output: "", error: message };
|
|
6113
|
-
}
|
|
6114
|
-
const spanStatus = result.status === "blocked" ? "blocked" : result.status === "failed" ? "failed" : "complete";
|
|
6115
|
-
statePersistence.closeAgentSpan(span.span_id, spanStatus, { output_valid: spanStatus === "complete" });
|
|
6116
|
-
progress.push({
|
|
6117
|
-
stage: stage.agent,
|
|
6118
|
-
status: spanStatus,
|
|
6119
|
-
output: result.output,
|
|
6120
|
-
error: result.error
|
|
6121
|
-
});
|
|
6122
|
-
if (spanStatus === "failed" || spanStatus === "blocked") {
|
|
6123
|
-
return {
|
|
6124
|
-
span_id: span.span_id,
|
|
6125
|
-
run_id: runCtx.runId,
|
|
6126
|
-
status: spanStatus,
|
|
6127
|
-
output: result.output,
|
|
6128
|
-
error: result.error,
|
|
6129
|
-
pipeline_progress: progress
|
|
6130
|
-
};
|
|
6131
|
-
}
|
|
6132
|
-
previousOutput = result.output;
|
|
6133
|
-
}
|
|
6134
|
-
return {
|
|
6135
|
-
span_id: "",
|
|
6136
|
-
run_id: runCtx.runId,
|
|
6137
|
-
status: "complete",
|
|
6138
|
-
output: previousOutput,
|
|
6139
|
-
pipeline_progress: progress
|
|
6140
|
-
};
|
|
6141
|
-
}
|
|
6142
|
-
return {
|
|
6143
|
-
span_id: "",
|
|
6144
|
-
run_id: runCtx.runId,
|
|
6145
|
-
status: "blocked",
|
|
6146
|
-
output: "",
|
|
6147
|
-
error: `Delegation mode "${resolvedMode}" is not supported by this version of FlowDeck. Use mode: "direct" for now. For multi-stage work, make sequential "direct" delegate calls from the orchestrator and pass each stage's output via context.previousOutput.`
|
|
6148
|
-
};
|
|
6149
|
-
} catch (error) {
|
|
6150
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
6151
|
-
return {
|
|
6152
|
-
span_id: "",
|
|
6153
|
-
run_id: runCtx.runId,
|
|
6154
|
-
status: "failed",
|
|
6155
|
-
output: "",
|
|
6156
|
-
error: message
|
|
6157
|
-
};
|
|
6158
|
-
} finally {
|
|
6159
|
-
releaseDelegation(runCtx.runId, childRunId);
|
|
6160
|
-
}
|
|
6161
|
-
}
|
|
6162
|
-
});
|
|
6163
|
-
}
|
|
6164
|
-
function collectTextFromParts(parts) {
|
|
6165
|
-
if (!Array.isArray(parts))
|
|
6166
|
-
return "";
|
|
6167
|
-
return parts.filter((part) => {
|
|
6168
|
-
if (!part || typeof part !== "object")
|
|
6169
|
-
return false;
|
|
6170
|
-
const type = "type" in part ? part.type : undefined;
|
|
6171
|
-
return type === "text" || type === "step-finish";
|
|
6172
|
-
}).map((part) => {
|
|
6173
|
-
const text = "text" in part && typeof part.text === "string" ? part.text : undefined;
|
|
6174
|
-
const content = "content" in part && typeof part.content === "string" ? part.content : undefined;
|
|
6175
|
-
return text ?? content ?? "";
|
|
6176
|
-
}).filter((text) => text.length > 0).join(`
|
|
6177
|
-
`);
|
|
6178
|
-
}
|
|
6179
|
-
function extractTextOutput(promptRes) {
|
|
6180
|
-
const data = promptRes?.data;
|
|
6181
|
-
for (const parts of [data?.parts, data?.message?.parts, data?.info?.message?.parts]) {
|
|
6182
|
-
const output = collectTextFromParts(parts);
|
|
6183
|
-
if (output)
|
|
6184
|
-
return output;
|
|
6185
|
-
}
|
|
6186
|
-
return "";
|
|
6187
|
-
}
|
|
6188
|
-
function extractMessageContent(message) {
|
|
6189
|
-
if (!message || typeof message !== "object")
|
|
6190
|
-
return "";
|
|
6191
|
-
if ("content" in message && typeof message.content === "string")
|
|
6192
|
-
return message.content;
|
|
6193
|
-
if ("parts" in message)
|
|
6194
|
-
return collectTextFromParts(message.parts);
|
|
6195
|
-
return "";
|
|
6196
|
-
}
|
|
6197
|
-
async function fetchFallbackOutput(client, childId) {
|
|
6198
|
-
const sessionClient = client.session;
|
|
6199
|
-
if (typeof sessionClient.messages !== "function")
|
|
6200
|
-
return "";
|
|
6201
|
-
const messageRes = await sessionClient.messages({ path: { id: childId } }).catch(() => null);
|
|
6202
|
-
const data = messageRes?.data;
|
|
6203
|
-
const messages = Array.isArray(data) ? data : Array.isArray(data?.messages) ? data.messages : [];
|
|
6204
|
-
for (let index = messages.length - 1;index >= 0; index -= 1) {
|
|
6205
|
-
const message = messages[index];
|
|
6206
|
-
if (!message || typeof message !== "object")
|
|
6207
|
-
continue;
|
|
6208
|
-
const role = "role" in message ? message.role : undefined;
|
|
6209
|
-
const author = "author" in message ? message.author : undefined;
|
|
6210
|
-
if (role === "assistant" || author === "assistant") {
|
|
6211
|
-
return extractMessageContent(message);
|
|
6212
|
-
}
|
|
6213
|
-
}
|
|
6214
|
-
return "";
|
|
6215
|
-
}
|
|
6216
|
-
function extractErrorMessage(error) {
|
|
6217
|
-
if (error instanceof Error)
|
|
6218
|
-
return error.message;
|
|
6219
|
-
if (typeof error === "string")
|
|
6220
|
-
return error;
|
|
6221
|
-
if (error && typeof error === "object" && "message" in error)
|
|
6222
|
-
return String(error.message);
|
|
6223
|
-
return String(error);
|
|
6224
|
-
}
|
|
6225
|
-
async function executeDirectDelegation(client, ctx, agent, task, span, runCtx, statePersistence, context) {
|
|
6226
|
-
const createRes = await client.session.create({
|
|
6227
|
-
body: { parentID: ctx.sessionID, title: `Delegate: ${agent}` },
|
|
6228
|
-
query: { directory: ctx.directory }
|
|
6229
|
-
});
|
|
6230
|
-
if (createRes.error || !createRes.data?.id) {
|
|
6231
|
-
return {
|
|
6232
|
-
status: "failed",
|
|
6233
|
-
output: "",
|
|
6234
|
-
error: extractErrorMessage(createRes.error ?? "Failed to create delegate session")
|
|
6235
|
-
};
|
|
6236
|
-
}
|
|
6237
|
-
const childId = createRes.data.id;
|
|
6238
|
-
const timeoutMs = Math.min(typeof context?.timeoutMs === "number" ? context.timeoutMs : DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS);
|
|
6239
|
-
statePersistence.updateRunContext({
|
|
6240
|
-
...runCtx,
|
|
6241
|
-
sessionID: childId,
|
|
6242
|
-
currentSpanId: span.span_id
|
|
6243
|
-
});
|
|
6244
|
-
const parts = [
|
|
6245
|
-
{ type: "text", text: buildDelegatePrompt(agent, task, context) }
|
|
6246
|
-
];
|
|
6247
|
-
let promptRes;
|
|
6248
|
-
try {
|
|
6249
|
-
promptRes = await Promise.race([
|
|
6250
|
-
client.session.prompt({
|
|
6251
|
-
path: { id: childId },
|
|
6252
|
-
body: { agent, parts },
|
|
6253
|
-
query: { directory: ctx.directory }
|
|
6254
|
-
}),
|
|
6255
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error("Delegate session timed out")), timeoutMs))
|
|
6256
|
-
]);
|
|
6257
|
-
} catch (error) {
|
|
6258
|
-
if (error instanceof Error && error.message === "Delegate session timed out") {
|
|
6259
|
-
const sessionClient = client.session;
|
|
6260
|
-
await sessionClient.abort?.({ path: { id: childId } }).catch(() => {});
|
|
6261
|
-
return {
|
|
6262
|
-
status: "failed",
|
|
6263
|
-
output: "",
|
|
6264
|
-
error: `Delegate session ${childId} (agent: ${agent}) timed out after ${timeoutMs}ms.`
|
|
6265
|
-
};
|
|
6266
|
-
}
|
|
6267
|
-
throw error;
|
|
6268
|
-
}
|
|
6269
|
-
if (process.env.FLOWDECK_DEBUG_DELEGATE) {
|
|
6270
|
-
console.error("[flowdeck delegate] prompt data keys", Object.keys(promptRes.data ?? {}));
|
|
6271
|
-
console.error("[flowdeck delegate] prompt data", JSON.stringify(promptRes.data));
|
|
6272
|
-
}
|
|
6273
|
-
if (promptRes.error) {
|
|
6274
|
-
return {
|
|
6275
|
-
status: "failed",
|
|
6276
|
-
output: "",
|
|
6277
|
-
error: extractErrorMessage(promptRes.error)
|
|
6278
|
-
};
|
|
6279
|
-
}
|
|
6280
|
-
const output = extractTextOutput(promptRes) || await fetchFallbackOutput(client, childId);
|
|
6281
|
-
if (!output) {
|
|
6282
|
-
return {
|
|
6283
|
-
status: "failed",
|
|
6284
|
-
output: "",
|
|
6285
|
-
error: `Subagent session ${childId} (agent: ${agent}) produced no text output after prompt + message fetch. This may indicate the subagent ended without a text summary, or the SDK response shape changed — check FLOWDECK_DEBUG_DELEGATE logs.`
|
|
6286
|
-
};
|
|
6287
|
-
}
|
|
6288
|
-
return {
|
|
6289
|
-
status: "complete",
|
|
6290
|
-
output
|
|
6291
|
-
};
|
|
6292
|
-
}
|
|
6293
|
-
function buildDelegatePrompt(agent, task, context) {
|
|
6294
|
-
const lines = [
|
|
6295
|
-
`You are @${agent}.`,
|
|
6296
|
-
"",
|
|
6297
|
-
"Task:",
|
|
6298
|
-
task
|
|
6299
|
-
];
|
|
6300
|
-
if (context && Object.keys(context).length > 0) {
|
|
6301
|
-
lines.push("", "Context:", JSON.stringify(context, null, 2));
|
|
6302
|
-
}
|
|
6303
|
-
lines.push("", "Do the work above. Return a concise summary of what you did and any results.");
|
|
6304
|
-
return lines.join(`
|
|
6305
|
-
`);
|
|
6306
|
-
}
|
|
6307
|
-
|
|
6308
5746
|
// src/hooks/notifications.ts
|
|
6309
5747
|
import { execFile } from "child_process";
|
|
6310
5748
|
var INTERACTIVE_COMMANDS = new Set([
|
|
@@ -6425,13 +5863,13 @@ class NotificationController {
|
|
|
6425
5863
|
return this.lastNotifiedKey;
|
|
6426
5864
|
}
|
|
6427
5865
|
}
|
|
6428
|
-
function notifyPermissionNeeded(
|
|
6429
|
-
notify("FlowDeck Permission Required", `Agent needs approval to use tool: ${
|
|
5866
|
+
function notifyPermissionNeeded(tool13) {
|
|
5867
|
+
notify("FlowDeck Permission Required", `Agent needs approval to use tool: ${tool13}`, "critical");
|
|
6430
5868
|
}
|
|
6431
5869
|
|
|
6432
5870
|
// src/hooks/shell-env-hook.ts
|
|
6433
|
-
import { existsSync as
|
|
6434
|
-
import { join as
|
|
5871
|
+
import { existsSync as existsSync16, readFileSync as readFileSync16 } from "fs";
|
|
5872
|
+
import { join as join16 } from "path";
|
|
6435
5873
|
import { createRequire } from "module";
|
|
6436
5874
|
var _version;
|
|
6437
5875
|
function getVersion() {
|
|
@@ -6467,7 +5905,7 @@ var MARKER_TO_LANG = {
|
|
|
6467
5905
|
};
|
|
6468
5906
|
function detectPackageManager(root) {
|
|
6469
5907
|
for (const [lockfile, pm] of Object.entries(LOCKFILE_TO_PM)) {
|
|
6470
|
-
if (
|
|
5908
|
+
if (existsSync16(join16(root, lockfile)))
|
|
6471
5909
|
return pm;
|
|
6472
5910
|
}
|
|
6473
5911
|
return;
|
|
@@ -6476,7 +5914,7 @@ function detectLanguages(root) {
|
|
|
6476
5914
|
const langs = [];
|
|
6477
5915
|
const seen = new Set;
|
|
6478
5916
|
for (const [marker, lang] of Object.entries(MARKER_TO_LANG)) {
|
|
6479
|
-
if (!seen.has(lang) &&
|
|
5917
|
+
if (!seen.has(lang) && existsSync16(join16(root, marker))) {
|
|
6480
5918
|
langs.push(lang);
|
|
6481
5919
|
seen.add(lang);
|
|
6482
5920
|
}
|
|
@@ -6484,11 +5922,11 @@ function detectLanguages(root) {
|
|
|
6484
5922
|
return langs;
|
|
6485
5923
|
}
|
|
6486
5924
|
function readCurrentPhase(root) {
|
|
6487
|
-
const statePath3 =
|
|
6488
|
-
if (!
|
|
5925
|
+
const statePath3 = join16(root, ".planning", "STATE.md");
|
|
5926
|
+
if (!existsSync16(statePath3))
|
|
6489
5927
|
return;
|
|
6490
5928
|
try {
|
|
6491
|
-
const content =
|
|
5929
|
+
const content = readFileSync16(statePath3, "utf-8");
|
|
6492
5930
|
const match = content.match(/phase:\s*(\S+)/i);
|
|
6493
5931
|
return match?.[1];
|
|
6494
5932
|
} catch {
|
|
@@ -6593,8 +6031,8 @@ function createSessionIdleHook(client, tracker) {
|
|
|
6593
6031
|
}
|
|
6594
6032
|
|
|
6595
6033
|
// src/hooks/compaction-hook.ts
|
|
6596
|
-
import { existsSync as
|
|
6597
|
-
import { join as
|
|
6034
|
+
import { existsSync as existsSync17, readFileSync as readFileSync17 } from "fs";
|
|
6035
|
+
import { join as join17 } from "path";
|
|
6598
6036
|
var STRUCTURED_SUMMARY_PROMPT = `
|
|
6599
6037
|
When summarizing this session, you MUST include the following sections:
|
|
6600
6038
|
|
|
@@ -6635,10 +6073,10 @@ For each: agent name, status, description, session_id.
|
|
|
6635
6073
|
var _lastInjected = new Map;
|
|
6636
6074
|
function readPlanningState2(directory) {
|
|
6637
6075
|
const sp = statePath(directory);
|
|
6638
|
-
if (!
|
|
6076
|
+
if (!existsSync17(sp))
|
|
6639
6077
|
return null;
|
|
6640
6078
|
try {
|
|
6641
|
-
const content =
|
|
6079
|
+
const content = readFileSync17(sp, "utf-8");
|
|
6642
6080
|
const parsed = parseState(content);
|
|
6643
6081
|
const version = typeof parsed.summaryVersion === "number" ? parsed.summaryVersion : 0;
|
|
6644
6082
|
return { content: content.slice(0, 1500), version };
|
|
@@ -6675,15 +6113,15 @@ function createCompactionHook(ctx, tracker, promptFragment) {
|
|
|
6675
6113
|
sections.push(`_State unchanged since last compaction. summaryVersion=${currentStateVersion}_`);
|
|
6676
6114
|
sections.push("");
|
|
6677
6115
|
}
|
|
6678
|
-
const indexPath2 =
|
|
6679
|
-
if (indexChanged &&
|
|
6116
|
+
const indexPath2 = join17(ctx.directory, ".planning", "CODEBASE_INDEX.md");
|
|
6117
|
+
if (indexChanged && existsSync17(indexPath2)) {
|
|
6680
6118
|
try {
|
|
6681
|
-
const indexContent =
|
|
6119
|
+
const indexContent = readFileSync17(indexPath2, "utf-8");
|
|
6682
6120
|
const indexSummary = "\n## Codebase Index\n```\n" + indexContent.slice(0, 800) + "\n```\n";
|
|
6683
6121
|
sections.push(indexSummary);
|
|
6684
6122
|
sections.push("");
|
|
6685
6123
|
} catch {}
|
|
6686
|
-
} else if (
|
|
6124
|
+
} else if (existsSync17(indexPath2)) {
|
|
6687
6125
|
sections.push(`## Codebase Index (unchanged, v${currentIndexVersion})`);
|
|
6688
6126
|
sections.push(`_Index unchanged since last compaction. summaryVersion=${currentIndexVersion}_`);
|
|
6689
6127
|
sections.push("");
|
|
@@ -6860,23 +6298,23 @@ function createFlowDeckMcps() {
|
|
|
6860
6298
|
}
|
|
6861
6299
|
|
|
6862
6300
|
// src/config/loader.ts
|
|
6863
|
-
import { existsSync as
|
|
6864
|
-
import { join as
|
|
6301
|
+
import { existsSync as existsSync18, readFileSync as readFileSync18 } from "fs";
|
|
6302
|
+
import { join as join18 } from "path";
|
|
6865
6303
|
import { homedir } from "os";
|
|
6866
6304
|
var CONFIG_FILENAME = "flowdeck.json";
|
|
6867
6305
|
function getGlobalConfigDir() {
|
|
6868
|
-
return process.env.OPENCODE_CONFIG_DIR || (process.env.XDG_CONFIG_HOME ?
|
|
6306
|
+
return process.env.OPENCODE_CONFIG_DIR || (process.env.XDG_CONFIG_HOME ? join18(process.env.XDG_CONFIG_HOME, "opencode") : join18(homedir(), ".config", "opencode"));
|
|
6869
6307
|
}
|
|
6870
6308
|
function loadFlowDeckConfig(directory) {
|
|
6871
6309
|
const candidates = [];
|
|
6872
6310
|
if (directory) {
|
|
6873
|
-
candidates.push(
|
|
6311
|
+
candidates.push(join18(directory, ".opencode", CONFIG_FILENAME));
|
|
6874
6312
|
}
|
|
6875
|
-
candidates.push(
|
|
6313
|
+
candidates.push(join18(getGlobalConfigDir(), CONFIG_FILENAME));
|
|
6876
6314
|
for (const configPath of candidates) {
|
|
6877
|
-
if (
|
|
6315
|
+
if (existsSync18(configPath)) {
|
|
6878
6316
|
try {
|
|
6879
|
-
const content =
|
|
6317
|
+
const content = readFileSync18(configPath, "utf-8");
|
|
6880
6318
|
return JSON.parse(content);
|
|
6881
6319
|
} catch {}
|
|
6882
6320
|
}
|
|
@@ -6899,8 +6337,8 @@ function resolveDesignFirstConfig(config) {
|
|
|
6899
6337
|
};
|
|
6900
6338
|
}
|
|
6901
6339
|
// src/services/context-ingress.ts
|
|
6902
|
-
import { existsSync as
|
|
6903
|
-
import { join as
|
|
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";
|
|
6904
6342
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
6905
6343
|
import { dirname as dirname3 } from "path";
|
|
6906
6344
|
|
|
@@ -7025,8 +6463,8 @@ function computeLanguageSnapshot(projectRoot) {
|
|
|
7025
6463
|
let maxMtime = 0;
|
|
7026
6464
|
const present = [];
|
|
7027
6465
|
for (const file of INDICATOR_FILES) {
|
|
7028
|
-
const path =
|
|
7029
|
-
if (
|
|
6466
|
+
const path = join19(projectRoot, file);
|
|
6467
|
+
if (existsSync19(path)) {
|
|
7030
6468
|
present.push(file);
|
|
7031
6469
|
try {
|
|
7032
6470
|
const stat = statSync(path);
|
|
@@ -7051,14 +6489,14 @@ function getCachedLanguages(projectRoot) {
|
|
|
7051
6489
|
function computeSkillsSnapshot(skillsDir) {
|
|
7052
6490
|
let maxMtime = 0;
|
|
7053
6491
|
const entries = [];
|
|
7054
|
-
if (
|
|
6492
|
+
if (existsSync19(skillsDir)) {
|
|
7055
6493
|
try {
|
|
7056
6494
|
for (const entry of readdirSync3(skillsDir, { withFileTypes: true })) {
|
|
7057
6495
|
if (!entry.isDirectory())
|
|
7058
6496
|
continue;
|
|
7059
6497
|
entries.push(entry.name);
|
|
7060
6498
|
try {
|
|
7061
|
-
const stat = statSync(
|
|
6499
|
+
const stat = statSync(join19(skillsDir, entry.name));
|
|
7062
6500
|
if (stat.mtimeMs > maxMtime) {
|
|
7063
6501
|
maxMtime = stat.mtimeMs;
|
|
7064
6502
|
}
|
|
@@ -7075,7 +6513,7 @@ function getCachedSkillNames(skillsDir) {
|
|
|
7075
6513
|
return cached.names;
|
|
7076
6514
|
}
|
|
7077
6515
|
const names = [];
|
|
7078
|
-
if (
|
|
6516
|
+
if (existsSync19(skillsDir)) {
|
|
7079
6517
|
try {
|
|
7080
6518
|
for (const entry of readdirSync3(skillsDir, { withFileTypes: true })) {
|
|
7081
6519
|
if (entry.isDirectory()) {
|
|
@@ -7259,9 +6697,9 @@ class ContextIngressService {
|
|
|
7259
6697
|
}
|
|
7260
6698
|
persistBudgetSnapshot(ctx) {
|
|
7261
6699
|
try {
|
|
7262
|
-
const cacheDir =
|
|
7263
|
-
if (!
|
|
7264
|
-
|
|
6700
|
+
const cacheDir = join19(ctx.directory, ".planning", "cache");
|
|
6701
|
+
if (!existsSync19(cacheDir)) {
|
|
6702
|
+
mkdirSync11(cacheDir, { recursive: true });
|
|
7265
6703
|
}
|
|
7266
6704
|
const snapshot = {
|
|
7267
6705
|
sessionID: ctx.sessionID,
|
|
@@ -7271,7 +6709,7 @@ class ContextIngressService {
|
|
|
7271
6709
|
componentBudgets: this.getComponentBudgets(ctx),
|
|
7272
6710
|
writtenAt: new Date().toISOString()
|
|
7273
6711
|
};
|
|
7274
|
-
|
|
6712
|
+
writeFileSync12(join19(cacheDir, "latest-context.json"), JSON.stringify(snapshot, null, 2), "utf-8");
|
|
7275
6713
|
} catch (error) {
|
|
7276
6714
|
const message = error instanceof Error ? error.message : String(error);
|
|
7277
6715
|
console.error(`[context-ingress] failed to persist budget snapshot: ${message}`);
|
|
@@ -7377,12 +6815,12 @@ class ContextIngressService {
|
|
|
7377
6815
|
return this._assembledBySession.get(sessionID);
|
|
7378
6816
|
}
|
|
7379
6817
|
readPlanContent(projectRoot, state) {
|
|
7380
|
-
const planningDir2 =
|
|
7381
|
-
const planPath =
|
|
7382
|
-
if (!
|
|
6818
|
+
const planningDir2 = join19(projectRoot, ".planning");
|
|
6819
|
+
const planPath = join19(planningDir2, "PLAN.md");
|
|
6820
|
+
if (!existsSync19(planPath))
|
|
7383
6821
|
return "";
|
|
7384
6822
|
try {
|
|
7385
|
-
let content =
|
|
6823
|
+
let content = readFileSync19(planPath, "utf-8");
|
|
7386
6824
|
if (content.length > this.options.planTruncateThreshold) {
|
|
7387
6825
|
content = `${content.slice(0, this.options.planTruncateTo)}
|
|
7388
6826
|
|
|
@@ -7394,28 +6832,28 @@ class ContextIngressService {
|
|
|
7394
6832
|
}
|
|
7395
6833
|
}
|
|
7396
6834
|
readCodebaseDocs(projectRoot) {
|
|
7397
|
-
const codebaseDir2 =
|
|
7398
|
-
if (!
|
|
6835
|
+
const codebaseDir2 = join19(projectRoot, ".codebase");
|
|
6836
|
+
if (!existsSync19(codebaseDir2))
|
|
7399
6837
|
return {};
|
|
7400
6838
|
const docs = {};
|
|
7401
6839
|
try {
|
|
7402
6840
|
for (const file of readdirSync3(codebaseDir2)) {
|
|
7403
6841
|
if (!file.endsWith(".md"))
|
|
7404
6842
|
continue;
|
|
7405
|
-
const filePath =
|
|
6843
|
+
const filePath = join19(codebaseDir2, file);
|
|
7406
6844
|
try {
|
|
7407
|
-
docs[file] =
|
|
6845
|
+
docs[file] = readFileSync19(filePath, "utf-8");
|
|
7408
6846
|
} catch {}
|
|
7409
6847
|
}
|
|
7410
6848
|
} catch {}
|
|
7411
6849
|
return docs;
|
|
7412
6850
|
}
|
|
7413
6851
|
readRecentEvents(projectRoot) {
|
|
7414
|
-
const eventsPath =
|
|
7415
|
-
if (!
|
|
6852
|
+
const eventsPath = join19(projectRoot, ".opencode", "flowdeck-events.jsonl");
|
|
6853
|
+
if (!existsSync19(eventsPath))
|
|
7416
6854
|
return [];
|
|
7417
6855
|
try {
|
|
7418
|
-
const content =
|
|
6856
|
+
const content = readFileSync19(eventsPath, "utf-8");
|
|
7419
6857
|
const cutoff = Date.now() - this.options.eventMaxAgeMinutes * 60 * 1000;
|
|
7420
6858
|
const events = [];
|
|
7421
6859
|
for (const line of content.split(`
|
|
@@ -7476,8 +6914,9 @@ class ContextIngressService {
|
|
|
7476
6914
|
}
|
|
7477
6915
|
selectRelevantRules(projectRoot, description, state, route, currentStage) {
|
|
7478
6916
|
const __dir = dirname3(fileURLToPath2(import.meta.url));
|
|
7479
|
-
const rulesDir =
|
|
7480
|
-
|
|
6917
|
+
const rulesDir = join19(__dir, "..", "rules");
|
|
6918
|
+
const sharedDir = join19(__dir, "..", "agents", "shared");
|
|
6919
|
+
if (!existsSync19(rulesDir))
|
|
7481
6920
|
return [];
|
|
7482
6921
|
const stage = currentStage ?? this.inferStageFromRoute(route);
|
|
7483
6922
|
const languages = getCachedLanguages(projectRoot);
|
|
@@ -7489,6 +6928,17 @@ class ContextIngressService {
|
|
|
7489
6928
|
}
|
|
7490
6929
|
const seen = new Set;
|
|
7491
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
|
+
}
|
|
7492
6942
|
for (const rule of selection.selected) {
|
|
7493
6943
|
const name = basename2(rule.path);
|
|
7494
6944
|
if (seen.has(name))
|
|
@@ -7503,8 +6953,8 @@ class ContextIngressService {
|
|
|
7503
6953
|
}
|
|
7504
6954
|
selectRelevantSkills(description) {
|
|
7505
6955
|
const __dir = dirname3(fileURLToPath2(import.meta.url));
|
|
7506
|
-
const skillsDir =
|
|
7507
|
-
if (!
|
|
6956
|
+
const skillsDir = join19(__dir, "..", "skills");
|
|
6957
|
+
if (!existsSync19(skillsDir))
|
|
7508
6958
|
return [];
|
|
7509
6959
|
const names = getCachedSkillNames(skillsDir);
|
|
7510
6960
|
return scoreSkills(names, description);
|
|
@@ -7515,7 +6965,7 @@ function createContextIngressService(options) {
|
|
|
7515
6965
|
}
|
|
7516
6966
|
|
|
7517
6967
|
// src/services/harness-policy.ts
|
|
7518
|
-
import { randomUUID as
|
|
6968
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
7519
6969
|
|
|
7520
6970
|
// src/services/loop-detector.ts
|
|
7521
6971
|
import { resolve as resolve2 } from "path";
|
|
@@ -7602,39 +7052,39 @@ function collapseWhitespace(input) {
|
|
|
7602
7052
|
return input.replace(/\s+/g, " ").trim();
|
|
7603
7053
|
}
|
|
7604
7054
|
function normalizeAction(toolName, args) {
|
|
7605
|
-
const
|
|
7606
|
-
if (
|
|
7055
|
+
const tool13 = toolName.toLowerCase();
|
|
7056
|
+
if (tool13 === "bash" || tool13 === "shell") {
|
|
7607
7057
|
const command = typeof args.command === "string" ? args.command : "";
|
|
7608
7058
|
const normalized = collapseWhitespace(resolveEnvVars(command)).toLowerCase();
|
|
7609
7059
|
return `shell:${normalized}`;
|
|
7610
7060
|
}
|
|
7611
|
-
if (
|
|
7061
|
+
if (tool13 === "read" || tool13 === "view") {
|
|
7612
7062
|
const filePath = typeof args.filePath === "string" ? args.filePath : "";
|
|
7613
7063
|
try {
|
|
7614
|
-
return `${
|
|
7064
|
+
return `${tool13}:${resolve2(filePath || "")}`;
|
|
7615
7065
|
} catch {
|
|
7616
|
-
return `${
|
|
7066
|
+
return `${tool13}:${filePath}`;
|
|
7617
7067
|
}
|
|
7618
7068
|
}
|
|
7619
|
-
if (
|
|
7069
|
+
if (tool13 === "write" || tool13 === "edit") {
|
|
7620
7070
|
const filePath = typeof args.filePath === "string" ? args.filePath : "";
|
|
7621
7071
|
try {
|
|
7622
|
-
return `${
|
|
7072
|
+
return `${tool13}:${resolve2(filePath || "")}`;
|
|
7623
7073
|
} catch {
|
|
7624
|
-
return `${
|
|
7074
|
+
return `${tool13}:${filePath}`;
|
|
7625
7075
|
}
|
|
7626
7076
|
}
|
|
7627
|
-
if (
|
|
7077
|
+
if (tool13 === "grep" || tool13 === "glob" || tool13 === "search") {
|
|
7628
7078
|
const pattern = typeof args.pattern === "string" ? args.pattern : "";
|
|
7629
7079
|
const path = typeof args.path === "string" ? args.path : "";
|
|
7630
|
-
return `${
|
|
7080
|
+
return `${tool13}:${pattern}:${resolve2(path || ".")}`;
|
|
7631
7081
|
}
|
|
7632
7082
|
const sorted = stableStringify(args);
|
|
7633
|
-
return `${
|
|
7083
|
+
return `${tool13}:${sorted}`;
|
|
7634
7084
|
}
|
|
7635
7085
|
function classifyObservation(toolName, previous, output, status, similarityThreshold) {
|
|
7636
7086
|
const outputPreview = getOutputPreview(output);
|
|
7637
|
-
const
|
|
7087
|
+
const tool13 = toolName.toLowerCase();
|
|
7638
7088
|
if (status === "blocked") {
|
|
7639
7089
|
return { observation: "same_result", outputHash: hashOutput(output), outputPreview };
|
|
7640
7090
|
}
|
|
@@ -7648,7 +7098,7 @@ function classifyObservation(toolName, previous, output, status, similarityThres
|
|
|
7648
7098
|
outputPreview: errorMessage.slice(0, 200)
|
|
7649
7099
|
};
|
|
7650
7100
|
}
|
|
7651
|
-
if (
|
|
7101
|
+
if (tool13 === "write" || tool13 === "edit") {
|
|
7652
7102
|
const contentHash = hashOutput(output);
|
|
7653
7103
|
return { observation: "new_information", outputHash: contentHash, outputPreview };
|
|
7654
7104
|
}
|
|
@@ -7659,7 +7109,7 @@ function classifyObservation(toolName, previous, output, status, similarityThres
|
|
|
7659
7109
|
if (outputHash === previous.outputHash) {
|
|
7660
7110
|
return { observation: "same_result", outputHash, outputPreview };
|
|
7661
7111
|
}
|
|
7662
|
-
if (NON_MUTATING_TOOLS.has(
|
|
7112
|
+
if (NON_MUTATING_TOOLS.has(tool13)) {
|
|
7663
7113
|
const similarity = lineSimilarity(outputPreview, previous.outputPreview);
|
|
7664
7114
|
if (similarity >= similarityThreshold) {
|
|
7665
7115
|
return { observation: "no_progress", outputHash, outputPreview };
|
|
@@ -7668,25 +7118,25 @@ function classifyObservation(toolName, previous, output, status, similarityThres
|
|
|
7668
7118
|
return { observation: "new_information", outputHash, outputPreview };
|
|
7669
7119
|
}
|
|
7670
7120
|
function redactForDisplay(toolName, normalizedKey) {
|
|
7671
|
-
const
|
|
7672
|
-
if (
|
|
7121
|
+
const tool13 = toolName.toLowerCase();
|
|
7122
|
+
if (tool13 === "bash" || tool13 === "shell") {
|
|
7673
7123
|
const idx2 = normalizedKey.indexOf(":");
|
|
7674
7124
|
const cmd = idx2 >= 0 ? normalizedKey.slice(idx2 + 1) : normalizedKey;
|
|
7675
7125
|
const preview = cmd.slice(0, 30);
|
|
7676
7126
|
const hash = djb2Hash(cmd);
|
|
7677
|
-
return `${
|
|
7127
|
+
return `${tool13}:"${preview}" (hash: ${hash})`;
|
|
7678
7128
|
}
|
|
7679
7129
|
const idx = normalizedKey.indexOf(":");
|
|
7680
7130
|
if (idx >= 0) {
|
|
7681
7131
|
const body = normalizedKey.slice(idx + 1);
|
|
7682
7132
|
if (body.startsWith("/") || body.startsWith(".") || body.includes("/")) {
|
|
7683
|
-
return `${
|
|
7133
|
+
return `${tool13}:"${body}"`;
|
|
7684
7134
|
}
|
|
7685
7135
|
const preview = body.slice(0, 30);
|
|
7686
7136
|
const hash = djb2Hash(body);
|
|
7687
|
-
return `${
|
|
7137
|
+
return `${tool13}:"${preview}" (hash: ${hash})`;
|
|
7688
7138
|
}
|
|
7689
|
-
return `${
|
|
7139
|
+
return `${tool13}:"${normalizedKey}"`;
|
|
7690
7140
|
}
|
|
7691
7141
|
|
|
7692
7142
|
class LoopDetector {
|
|
@@ -7864,8 +7314,7 @@ var CONTRACTS = [
|
|
|
7864
7314
|
"load-rules",
|
|
7865
7315
|
"list-rules",
|
|
7866
7316
|
"hash-edit",
|
|
7867
|
-
"failure-replay"
|
|
7868
|
-
"delegate"
|
|
7317
|
+
"failure-replay"
|
|
7869
7318
|
],
|
|
7870
7319
|
forbiddenActions: [
|
|
7871
7320
|
"write_file",
|
|
@@ -8369,13 +7818,13 @@ function resolveSupervisorConfig(directory) {
|
|
|
8369
7818
|
function isRegisteredCommand(name) {
|
|
8370
7819
|
return REGISTERED_COMMANDS.includes(name);
|
|
8371
7820
|
}
|
|
8372
|
-
function
|
|
7821
|
+
function isRegisteredAgent(name) {
|
|
8373
7822
|
return AGENT_NAMES.includes(name);
|
|
8374
7823
|
}
|
|
8375
7824
|
function isRegisteredTarget(name) {
|
|
8376
7825
|
if (isRegisteredCommand(name))
|
|
8377
7826
|
return { exists: true, type: "command" };
|
|
8378
|
-
if (
|
|
7827
|
+
if (isRegisteredAgent(name))
|
|
8379
7828
|
return { exists: true, type: "agent" };
|
|
8380
7829
|
return { exists: false, type: "agent" };
|
|
8381
7830
|
}
|
|
@@ -8617,8 +8066,8 @@ function reviewToolCall(directory, input) {
|
|
|
8617
8066
|
}
|
|
8618
8067
|
|
|
8619
8068
|
// src/hooks/tool-guard.ts
|
|
8620
|
-
import { existsSync as
|
|
8621
|
-
import { join as
|
|
8069
|
+
import { existsSync as existsSync20, readFileSync as readFileSync20 } from "fs";
|
|
8070
|
+
import { join as join20 } from "path";
|
|
8622
8071
|
|
|
8623
8072
|
// src/lib/task-routing.ts
|
|
8624
8073
|
var UI_HEAVY_KEYWORDS = [
|
|
@@ -8730,23 +8179,23 @@ function isBashCommandDangerous(cmd) {
|
|
|
8730
8179
|
}
|
|
8731
8180
|
return null;
|
|
8732
8181
|
}
|
|
8733
|
-
function isBlocked(
|
|
8734
|
-
const patterns = BLOCKED_PATTERNS[
|
|
8182
|
+
function isBlocked(tool13, args) {
|
|
8183
|
+
const patterns = BLOCKED_PATTERNS[tool13];
|
|
8735
8184
|
if (!patterns)
|
|
8736
8185
|
return null;
|
|
8737
|
-
if (
|
|
8186
|
+
if (tool13 === "bash") {
|
|
8738
8187
|
const cmd = args.command;
|
|
8739
8188
|
if (!cmd)
|
|
8740
8189
|
return null;
|
|
8741
8190
|
return isBashCommandDangerous(cmd);
|
|
8742
8191
|
}
|
|
8743
|
-
if (
|
|
8192
|
+
if (tool13 === "read" || tool13 === "write") {
|
|
8744
8193
|
const filePath = extractTargetPath(args);
|
|
8745
8194
|
if (!filePath)
|
|
8746
8195
|
return null;
|
|
8747
8196
|
for (const p of patterns) {
|
|
8748
8197
|
if (filePath.includes(p)) {
|
|
8749
|
-
return
|
|
8198
|
+
return tool13 === "read" ? `FLOWDECK: Access to "${p}" files is blocked.` : `FLOWDECK: Writing to "${p}" is blocked.`;
|
|
8750
8199
|
}
|
|
8751
8200
|
}
|
|
8752
8201
|
return null;
|
|
@@ -8754,11 +8203,11 @@ function isBlocked(tool14, args) {
|
|
|
8754
8203
|
return null;
|
|
8755
8204
|
}
|
|
8756
8205
|
function checkArchConstraint(directory, filePath) {
|
|
8757
|
-
const constraintsPath =
|
|
8758
|
-
if (!
|
|
8206
|
+
const constraintsPath = join20(codebaseDir(directory), "CONSTRAINTS.md");
|
|
8207
|
+
if (!existsSync20(constraintsPath))
|
|
8759
8208
|
return null;
|
|
8760
8209
|
try {
|
|
8761
|
-
const content =
|
|
8210
|
+
const content = readFileSync20(constraintsPath, "utf-8");
|
|
8762
8211
|
const match = content.match(/## Forbidden Paths\n([\s\S]*?)(?:\n##|$)/);
|
|
8763
8212
|
if (!match)
|
|
8764
8213
|
return null;
|
|
@@ -8799,9 +8248,9 @@ function isUiDesignApprovalRequired(directory) {
|
|
|
8799
8248
|
return !(state.design_stage === "handoff_complete" && state.design_approved);
|
|
8800
8249
|
}
|
|
8801
8250
|
const planPath = phasePlanPath(directory, state.phase || 1);
|
|
8802
|
-
if (!
|
|
8251
|
+
if (!existsSync20(planPath))
|
|
8803
8252
|
return false;
|
|
8804
|
-
const planContent =
|
|
8253
|
+
const planContent = readFileSync20(planPath, "utf-8");
|
|
8805
8254
|
if (!isUiHeavyTask(planContent))
|
|
8806
8255
|
return false;
|
|
8807
8256
|
return !(state.design_stage === "handoff_complete" && state.design_approved);
|
|
@@ -8819,15 +8268,15 @@ function deny(reason, escalationMessage, riskFlags = []) {
|
|
|
8819
8268
|
};
|
|
8820
8269
|
}
|
|
8821
8270
|
function evaluate(input) {
|
|
8822
|
-
const { directory, tool:
|
|
8823
|
-
if (
|
|
8271
|
+
const { directory, tool: tool13, args } = input;
|
|
8272
|
+
if (tool13 !== "bash" && tool13 !== "read" && tool13 !== "write" && tool13 !== "edit") {
|
|
8824
8273
|
return allow("Tool is not guardable");
|
|
8825
8274
|
}
|
|
8826
|
-
const blocked = isBlocked(
|
|
8275
|
+
const blocked = isBlocked(tool13, args);
|
|
8827
8276
|
if (blocked) {
|
|
8828
8277
|
return deny(blocked, blocked, ["dangerous-pattern"]);
|
|
8829
8278
|
}
|
|
8830
|
-
if (
|
|
8279
|
+
if (tool13 === "write" || tool13 === "edit") {
|
|
8831
8280
|
const phaseBlock = checkPhaseEnforcement(directory);
|
|
8832
8281
|
if (phaseBlock) {
|
|
8833
8282
|
const isAdvisory = phaseBlock.includes("[design-gate]: advisory");
|
|
@@ -8846,15 +8295,15 @@ function evaluate(input) {
|
|
|
8846
8295
|
}
|
|
8847
8296
|
|
|
8848
8297
|
// src/hooks/guard-rails.ts
|
|
8849
|
-
import { existsSync as
|
|
8850
|
-
import { join as
|
|
8298
|
+
import { existsSync as existsSync21, readFileSync as readFileSync21 } from "fs";
|
|
8299
|
+
import { join as join21 } from "path";
|
|
8851
8300
|
var PLANNING_DIR2 = ".planning";
|
|
8852
8301
|
var CONFIG_FILE = "config.json";
|
|
8853
8302
|
var STATE_FILE2 = "STATE.md";
|
|
8854
8303
|
function resolveExecutionMode(configPath, trustScore, volatility) {
|
|
8855
|
-
if (
|
|
8304
|
+
if (existsSync21(configPath)) {
|
|
8856
8305
|
try {
|
|
8857
|
-
const config = JSON.parse(
|
|
8306
|
+
const config = JSON.parse(readFileSync21(configPath, "utf-8"));
|
|
8858
8307
|
if (config.execution_mode === "review-only")
|
|
8859
8308
|
return "review-only";
|
|
8860
8309
|
if (config.execution_mode === "guarded")
|
|
@@ -8916,24 +8365,24 @@ function deny2(reason, escalationMessage, riskFlags = []) {
|
|
|
8916
8365
|
};
|
|
8917
8366
|
}
|
|
8918
8367
|
function evaluate2(input) {
|
|
8919
|
-
const { directory, tool:
|
|
8920
|
-
const planningDirPath =
|
|
8368
|
+
const { directory, tool: tool13, args } = input;
|
|
8369
|
+
const planningDirPath = join21(directory, PLANNING_DIR2);
|
|
8921
8370
|
const codebaseDirectory = codebaseDir(directory);
|
|
8922
|
-
const configPath =
|
|
8923
|
-
const statePath3 =
|
|
8371
|
+
const configPath = join21(planningDirPath, CONFIG_FILE);
|
|
8372
|
+
const statePath3 = join21(planningDirPath, STATE_FILE2);
|
|
8924
8373
|
const workspaceRoot = findWorkspaceRoot(directory);
|
|
8925
8374
|
if (workspaceRoot && directory !== workspaceRoot) {
|
|
8926
8375
|
const workspaceConfig = getWorkspaceConfig(directory);
|
|
8927
|
-
if (workspaceConfig && workspaceConfig.workspace_mode === "shared" && !
|
|
8376
|
+
if (workspaceConfig && workspaceConfig.workspace_mode === "shared" && !existsSync21(planningDirPath)) {
|
|
8928
8377
|
const msg = `No .planning/ in this sub-repo. Switch to workspace root: cd ${workspaceRoot}`;
|
|
8929
8378
|
return deny2(msg, `[flowdeck] BLOCK: ${msg}`, ["workspace-shared-mode"]);
|
|
8930
8379
|
}
|
|
8931
8380
|
}
|
|
8932
|
-
if (
|
|
8933
|
-
if (!
|
|
8381
|
+
if (tool13 === "write" || tool13 === "edit") {
|
|
8382
|
+
if (!existsSync21(planningDirPath)) {
|
|
8934
8383
|
return allow2("FlowDeck not initialized in this directory — skipping guard-rails");
|
|
8935
8384
|
}
|
|
8936
|
-
if (!
|
|
8385
|
+
if (!existsSync21(codebaseDirectory)) {
|
|
8937
8386
|
const msg = ".codebase/ not found. Run /fd-map-codebase to map the codebase.";
|
|
8938
8387
|
return allow2(msg, ["codebase-missing"]);
|
|
8939
8388
|
}
|
|
@@ -8963,7 +8412,7 @@ function evaluate2(input) {
|
|
|
8963
8412
|
const blockMessage = getBlockMessage(planningDirPath);
|
|
8964
8413
|
return deny2(blockMessage, `[flowdeck] BLOCK: ${blockMessage}`, ["plan-not-confirmed"]);
|
|
8965
8414
|
}
|
|
8966
|
-
if (
|
|
8415
|
+
if (tool13 === "bash") {
|
|
8967
8416
|
const cmd = String(args?.command || "");
|
|
8968
8417
|
for (const pattern of BUILD_DEPLOY_PATTERNS) {
|
|
8969
8418
|
if (cmd.includes(pattern)) {
|
|
@@ -8997,15 +8446,15 @@ function getDesignGateMessage(dir) {
|
|
|
8997
8446
|
}
|
|
8998
8447
|
function planSuggestsUiHeavy(dir, phase) {
|
|
8999
8448
|
const planPath = phasePlanPath(dir, phase);
|
|
9000
|
-
if (!
|
|
8449
|
+
if (!existsSync21(planPath))
|
|
9001
8450
|
return false;
|
|
9002
|
-
const planContent =
|
|
8451
|
+
const planContent = readFileSync21(planPath, "utf-8");
|
|
9003
8452
|
return isUiHeavyTask(planContent);
|
|
9004
8453
|
}
|
|
9005
8454
|
function effectiveSeverity(configPath, statePath3) {
|
|
9006
|
-
if (
|
|
8455
|
+
if (existsSync21(configPath)) {
|
|
9007
8456
|
try {
|
|
9008
|
-
const configContent =
|
|
8457
|
+
const configContent = readFileSync21(configPath, "utf-8");
|
|
9009
8458
|
const config = JSON.parse(configContent);
|
|
9010
8459
|
if (config.guard_enforcement === "warn")
|
|
9011
8460
|
return "warn";
|
|
@@ -9018,10 +8467,10 @@ function effectiveSeverity(configPath, statePath3) {
|
|
|
9018
8467
|
return getPlanConfirmed(statePath3) ? "block" : "warn";
|
|
9019
8468
|
}
|
|
9020
8469
|
function getPlanConfirmed(statePath3) {
|
|
9021
|
-
if (!
|
|
8470
|
+
if (!existsSync21(statePath3))
|
|
9022
8471
|
return false;
|
|
9023
8472
|
try {
|
|
9024
|
-
const content =
|
|
8473
|
+
const content = readFileSync21(statePath3, "utf-8");
|
|
9025
8474
|
const match = content.match(/plan_confirmed:\s*(true|false)/i);
|
|
9026
8475
|
return match ? match[1].toLowerCase() === "true" : false;
|
|
9027
8476
|
} catch {
|
|
@@ -9029,23 +8478,23 @@ function getPlanConfirmed(statePath3) {
|
|
|
9029
8478
|
}
|
|
9030
8479
|
}
|
|
9031
8480
|
function getWarningMessage(planningDir2) {
|
|
9032
|
-
if (!
|
|
8481
|
+
if (!existsSync21(join21(planningDir2, STATE_FILE2))) {
|
|
9033
8482
|
return "No STATE.md found. Run /fd-map-codebase then /fd-new-feature to start a feature.";
|
|
9034
8483
|
}
|
|
9035
8484
|
return "Plan not confirmed. Run /fd-plan and confirm to enable execution.";
|
|
9036
8485
|
}
|
|
9037
8486
|
function getBlockMessage(planningDir2) {
|
|
9038
|
-
if (!
|
|
8487
|
+
if (!existsSync21(join21(planningDir2, STATE_FILE2))) {
|
|
9039
8488
|
return "No STATE.md found. Run /fd-map-codebase then /fd-new-feature to start a feature.";
|
|
9040
8489
|
}
|
|
9041
8490
|
return "Plan not confirmed. Run /fd-plan and confirm to enable execution.";
|
|
9042
8491
|
}
|
|
9043
8492
|
|
|
9044
8493
|
// src/services/approval-manager.ts
|
|
9045
|
-
import { existsSync as
|
|
9046
|
-
import { join as
|
|
8494
|
+
import { existsSync as existsSync22, readFileSync as readFileSync22, writeFileSync as writeFileSync13, mkdirSync as mkdirSync12 } from "fs";
|
|
8495
|
+
import { join as join22 } from "path";
|
|
9047
8496
|
import { createHash as createHash4 } from "crypto";
|
|
9048
|
-
import { randomUUID
|
|
8497
|
+
import { randomUUID } from "crypto";
|
|
9049
8498
|
var APPROVAL_TTL_MS = 30 * 60 * 1000;
|
|
9050
8499
|
var SENSITIVE_PATTERNS = [
|
|
9051
8500
|
/auth/i,
|
|
@@ -9082,28 +8531,28 @@ function isSensitivePath(filePath) {
|
|
|
9082
8531
|
return SENSITIVE_PATTERNS.some((p) => p.test(filePath));
|
|
9083
8532
|
}
|
|
9084
8533
|
function approvalsPath(dir) {
|
|
9085
|
-
return
|
|
8534
|
+
return join22(codebaseDir(dir), "APPROVALS.json");
|
|
9086
8535
|
}
|
|
9087
8536
|
function loadStore(dir) {
|
|
9088
8537
|
const p = approvalsPath(dir);
|
|
9089
|
-
if (!
|
|
8538
|
+
if (!existsSync22(p))
|
|
9090
8539
|
return { requests: [] };
|
|
9091
8540
|
try {
|
|
9092
|
-
return JSON.parse(
|
|
8541
|
+
return JSON.parse(readFileSync22(p, "utf-8"));
|
|
9093
8542
|
} catch {
|
|
9094
8543
|
return { requests: [] };
|
|
9095
8544
|
}
|
|
9096
8545
|
}
|
|
9097
8546
|
function saveStore(dir, store) {
|
|
9098
8547
|
const cd = codebaseDir(dir);
|
|
9099
|
-
if (!
|
|
9100
|
-
|
|
9101
|
-
|
|
8548
|
+
if (!existsSync22(cd))
|
|
8549
|
+
mkdirSync12(cd, { recursive: true });
|
|
8550
|
+
writeFileSync13(approvalsPath(dir), JSON.stringify(store, null, 2), "utf-8");
|
|
9102
8551
|
}
|
|
9103
8552
|
function requestApproval(dir, run_id, trigger, reason, options = {}) {
|
|
9104
8553
|
const store = loadStore(dir);
|
|
9105
8554
|
const req = {
|
|
9106
|
-
id:
|
|
8555
|
+
id: randomUUID(),
|
|
9107
8556
|
run_id,
|
|
9108
8557
|
session_id: options.session_id ?? "session-0",
|
|
9109
8558
|
requested_at: new Date().toISOString(),
|
|
@@ -9130,16 +8579,16 @@ function computeContentHash(args) {
|
|
|
9130
8579
|
const payload = JSON.stringify(args, keys);
|
|
9131
8580
|
return createHash4("sha256").update(payload).digest("hex").slice(0, 16);
|
|
9132
8581
|
}
|
|
9133
|
-
function requestApprovalForTool(dir, run_id, session_id, agent,
|
|
8582
|
+
function requestApprovalForTool(dir, run_id, session_id, agent, tool13, args) {
|
|
9134
8583
|
const filePath = extractTargetPath2(args);
|
|
9135
8584
|
const isSensitive = filePath ? isSensitivePath(filePath) : false;
|
|
9136
|
-
return requestApproval(dir, run_id,
|
|
8585
|
+
return requestApproval(dir, run_id, tool13, `Approval required for tool "${tool13}"`, {
|
|
9137
8586
|
file_path: filePath,
|
|
9138
8587
|
risk_score: isSensitive ? 30 : 50,
|
|
9139
8588
|
session_id,
|
|
9140
8589
|
agent,
|
|
9141
8590
|
content_hash: computeContentHash(args),
|
|
9142
|
-
change_description: `Tool "${
|
|
8591
|
+
change_description: `Tool "${tool13}" requested on ${filePath || "unknown target"}`
|
|
9143
8592
|
});
|
|
9144
8593
|
}
|
|
9145
8594
|
function extractTargetPath2(args) {
|
|
@@ -9168,8 +8617,8 @@ function ask(reason, riskFlags = []) {
|
|
|
9168
8617
|
};
|
|
9169
8618
|
}
|
|
9170
8619
|
function evaluate3(input) {
|
|
9171
|
-
const { directory, tool:
|
|
9172
|
-
if (!WRITE_TOOLS.has(
|
|
8620
|
+
const { directory, tool: tool13, args } = input;
|
|
8621
|
+
if (!WRITE_TOOLS.has(tool13)) {
|
|
9173
8622
|
return allow3("Tool does not require approval");
|
|
9174
8623
|
}
|
|
9175
8624
|
const filePath = String(args?.path ?? args?.file_path ?? args?.filename ?? args?.filePath ?? "");
|
|
@@ -9193,23 +8642,23 @@ function evaluate3(input) {
|
|
|
9193
8642
|
}
|
|
9194
8643
|
|
|
9195
8644
|
// src/services/deadlock-detector.ts
|
|
9196
|
-
import { existsSync as
|
|
9197
|
-
import { join as
|
|
9198
|
-
import { randomUUID as
|
|
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";
|
|
9199
8648
|
|
|
9200
8649
|
// src/services/agent-trace-graph.ts
|
|
9201
|
-
import { existsSync as
|
|
9202
|
-
import { join as
|
|
9203
|
-
import { randomUUID as
|
|
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";
|
|
9204
8653
|
function agentSpansPath(dir) {
|
|
9205
|
-
return
|
|
8654
|
+
return join23(codebaseDir(dir), "AGENT_SPANS.jsonl");
|
|
9206
8655
|
}
|
|
9207
8656
|
function loadAllSpans(dir) {
|
|
9208
8657
|
const p = agentSpansPath(dir);
|
|
9209
|
-
if (!
|
|
8658
|
+
if (!existsSync23(p))
|
|
9210
8659
|
return [];
|
|
9211
8660
|
try {
|
|
9212
|
-
return
|
|
8661
|
+
return readFileSync23(p, "utf-8").trim().split(`
|
|
9213
8662
|
`).filter(Boolean).map((l) => JSON.parse(l));
|
|
9214
8663
|
} catch {
|
|
9215
8664
|
return [];
|
|
@@ -9218,18 +8667,18 @@ function loadAllSpans(dir) {
|
|
|
9218
8667
|
function saveAllSpans(dir, spans) {
|
|
9219
8668
|
const p = agentSpansPath(dir);
|
|
9220
8669
|
const cd = codebaseDir(dir);
|
|
9221
|
-
if (!
|
|
9222
|
-
|
|
9223
|
-
|
|
8670
|
+
if (!existsSync23(cd))
|
|
8671
|
+
mkdirSync13(cd, { recursive: true });
|
|
8672
|
+
writeFileSync14(p, spans.map((s) => JSON.stringify(s)).join(`
|
|
9224
8673
|
`) + `
|
|
9225
8674
|
`, "utf-8");
|
|
9226
8675
|
}
|
|
9227
8676
|
function openSpan(dir, opts) {
|
|
9228
8677
|
const cd = codebaseDir(dir);
|
|
9229
|
-
if (!
|
|
9230
|
-
|
|
8678
|
+
if (!existsSync23(cd))
|
|
8679
|
+
mkdirSync13(cd, { recursive: true });
|
|
9231
8680
|
const span = {
|
|
9232
|
-
span_id:
|
|
8681
|
+
span_id: randomUUID2(),
|
|
9233
8682
|
trace_id: opts.trace_id,
|
|
9234
8683
|
parent_span_id: opts.parent_span_id,
|
|
9235
8684
|
invoker: opts.invoker,
|
|
@@ -9245,7 +8694,7 @@ function openSpan(dir, opts) {
|
|
|
9245
8694
|
depth: opts.depth ?? 0,
|
|
9246
8695
|
model: opts.model
|
|
9247
8696
|
};
|
|
9248
|
-
|
|
8697
|
+
appendFileSync4(agentSpansPath(dir), JSON.stringify(span) + `
|
|
9249
8698
|
`, "utf-8");
|
|
9250
8699
|
return span;
|
|
9251
8700
|
}
|
|
@@ -9290,9 +8739,6 @@ function addSpanViolation(dir, span_id, violation) {
|
|
|
9290
8739
|
function recordContractViolation(dir, span_id, violation) {
|
|
9291
8740
|
addSpanViolation(dir, span_id, violation);
|
|
9292
8741
|
}
|
|
9293
|
-
function getSpan(dir, span_id) {
|
|
9294
|
-
return loadAllSpans(dir).findLast((s) => s.span_id === span_id) ?? null;
|
|
9295
|
-
}
|
|
9296
8742
|
function getTraceSpans(dir, trace_id) {
|
|
9297
8743
|
return loadAllSpans(dir).filter((s) => s.trace_id === trace_id);
|
|
9298
8744
|
}
|
|
@@ -9314,21 +8760,21 @@ function resolveConfig(directory) {
|
|
|
9314
8760
|
}
|
|
9315
8761
|
}
|
|
9316
8762
|
function deadlockSignalsPath(dir) {
|
|
9317
|
-
return
|
|
8763
|
+
return join24(codebaseDir(dir), "DEADLOCK_SIGNALS.jsonl");
|
|
9318
8764
|
}
|
|
9319
8765
|
function appendSignal(dir, signal) {
|
|
9320
8766
|
const cd = codebaseDir(dir);
|
|
9321
|
-
if (!
|
|
9322
|
-
|
|
9323
|
-
|
|
8767
|
+
if (!existsSync24(cd))
|
|
8768
|
+
mkdirSync14(cd, { recursive: true });
|
|
8769
|
+
appendFileSync5(deadlockSignalsPath(dir), JSON.stringify(signal) + `
|
|
9324
8770
|
`, "utf-8");
|
|
9325
8771
|
}
|
|
9326
8772
|
function getSignals(dir, trace_id) {
|
|
9327
8773
|
const p = deadlockSignalsPath(dir);
|
|
9328
|
-
if (!
|
|
8774
|
+
if (!existsSync24(p))
|
|
9329
8775
|
return [];
|
|
9330
8776
|
try {
|
|
9331
|
-
const all =
|
|
8777
|
+
const all = readFileSync24(p, "utf-8").trim().split(`
|
|
9332
8778
|
`).filter(Boolean).map((l) => JSON.parse(l));
|
|
9333
8779
|
return trace_id ? all.filter((s) => s.trace_id === trace_id) : all;
|
|
9334
8780
|
} catch {
|
|
@@ -9346,7 +8792,7 @@ function detectAgentBounce(dir, trace_id, cfg) {
|
|
|
9346
8792
|
if (count >= cfg.bounceThreshold) {
|
|
9347
8793
|
const [a, b] = pair.split("→");
|
|
9348
8794
|
return {
|
|
9349
|
-
signal_id:
|
|
8795
|
+
signal_id: randomUUID3(),
|
|
9350
8796
|
trace_id,
|
|
9351
8797
|
detected_at: new Date().toISOString(),
|
|
9352
8798
|
type: "agent_bounce",
|
|
@@ -9388,7 +8834,7 @@ function detectCircularDelegation(dir, trace_id, cfg) {
|
|
|
9388
8834
|
const cycle = findCycle(node, visited, [node]);
|
|
9389
8835
|
if (cycle) {
|
|
9390
8836
|
return {
|
|
9391
|
-
signal_id:
|
|
8837
|
+
signal_id: randomUUID3(),
|
|
9392
8838
|
trace_id,
|
|
9393
8839
|
detected_at: new Date().toISOString(),
|
|
9394
8840
|
type: "circular_delegation",
|
|
@@ -9416,7 +8862,7 @@ function detectStepRetryLoop(dir, trace_id, cfg) {
|
|
|
9416
8862
|
for (const [key, count] of Object.entries(stageCounts)) {
|
|
9417
8863
|
if (count >= cfg.retryLoopThreshold) {
|
|
9418
8864
|
return {
|
|
9419
|
-
signal_id:
|
|
8865
|
+
signal_id: randomUUID3(),
|
|
9420
8866
|
trace_id,
|
|
9421
8867
|
detected_at: new Date().toISOString(),
|
|
9422
8868
|
type: "step_retry_loop",
|
|
@@ -9438,7 +8884,7 @@ function detectStageStall(dir, trace_id, cfg) {
|
|
|
9438
8884
|
const elapsed = (now - new Date(span.started_at).getTime()) / 1000 / 60;
|
|
9439
8885
|
if (elapsed >= cfg.stageStallMinutes) {
|
|
9440
8886
|
return {
|
|
9441
|
-
signal_id:
|
|
8887
|
+
signal_id: randomUUID3(),
|
|
9442
8888
|
trace_id,
|
|
9443
8889
|
detected_at: new Date().toISOString(),
|
|
9444
8890
|
type: "stage_stall",
|
|
@@ -9475,29 +8921,29 @@ function isTraceStuck(dir, trace_id) {
|
|
|
9475
8921
|
|
|
9476
8922
|
// src/services/audit-log.ts
|
|
9477
8923
|
import {
|
|
9478
|
-
existsSync as
|
|
9479
|
-
mkdirSync as
|
|
9480
|
-
appendFileSync as
|
|
9481
|
-
readFileSync as
|
|
9482
|
-
writeFileSync as
|
|
8924
|
+
existsSync as existsSync25,
|
|
8925
|
+
mkdirSync as mkdirSync15,
|
|
8926
|
+
appendFileSync as appendFileSync6,
|
|
8927
|
+
readFileSync as readFileSync25,
|
|
8928
|
+
writeFileSync as writeFileSync15,
|
|
9483
8929
|
renameSync,
|
|
9484
8930
|
unlinkSync
|
|
9485
8931
|
} from "fs";
|
|
9486
|
-
import { join as
|
|
8932
|
+
import { join as join25 } from "path";
|
|
9487
8933
|
var AUDIT_FILE = "AUDIT.jsonl";
|
|
9488
8934
|
var ROTATE_LINE_COUNT = 1000;
|
|
9489
8935
|
function auditPath(directory) {
|
|
9490
|
-
return
|
|
8936
|
+
return join25(codebaseDir(directory), AUDIT_FILE);
|
|
9491
8937
|
}
|
|
9492
8938
|
function ensureDirectory(dir) {
|
|
9493
8939
|
const base = codebaseDir(dir);
|
|
9494
|
-
if (!
|
|
9495
|
-
|
|
8940
|
+
if (!existsSync25(base)) {
|
|
8941
|
+
mkdirSync15(base, { recursive: true });
|
|
9496
8942
|
}
|
|
9497
8943
|
}
|
|
9498
8944
|
function rotateIfNeeded(path, appLog) {
|
|
9499
8945
|
try {
|
|
9500
|
-
const content =
|
|
8946
|
+
const content = readFileSync25(path, "utf-8");
|
|
9501
8947
|
const lines = content.split(`
|
|
9502
8948
|
`).filter((line) => line.trim().length > 0);
|
|
9503
8949
|
if (lines.length <= ROTATE_LINE_COUNT)
|
|
@@ -9505,7 +8951,7 @@ function rotateIfNeeded(path, appLog) {
|
|
|
9505
8951
|
const backupPath = `${path}.backup`;
|
|
9506
8952
|
renameSync(path, backupPath);
|
|
9507
8953
|
const keep = lines.slice(-ROTATE_LINE_COUNT);
|
|
9508
|
-
|
|
8954
|
+
writeFileSync15(path, keep.join(`
|
|
9509
8955
|
`) + `
|
|
9510
8956
|
`, "utf-8");
|
|
9511
8957
|
try {
|
|
@@ -9521,16 +8967,16 @@ function rotateIfNeeded(path, appLog) {
|
|
|
9521
8967
|
function appendAuditEntry(directory, entry, appLog) {
|
|
9522
8968
|
ensureDirectory(directory);
|
|
9523
8969
|
const path = auditPath(directory);
|
|
9524
|
-
|
|
8970
|
+
appendFileSync6(path, JSON.stringify(entry) + `
|
|
9525
8971
|
`, "utf-8");
|
|
9526
8972
|
rotateIfNeeded(path, appLog);
|
|
9527
8973
|
}
|
|
9528
8974
|
function queryAudit(directory, filter) {
|
|
9529
8975
|
const path = auditPath(directory);
|
|
9530
|
-
if (!
|
|
8976
|
+
if (!existsSync25(path))
|
|
9531
8977
|
return [];
|
|
9532
8978
|
try {
|
|
9533
|
-
const lines =
|
|
8979
|
+
const lines = readFileSync25(path, "utf-8").split(`
|
|
9534
8980
|
`).filter((line) => line.trim().length > 0);
|
|
9535
8981
|
const results = [];
|
|
9536
8982
|
for (const line of lines) {
|
|
@@ -9596,7 +9042,7 @@ function validationToDecision(result) {
|
|
|
9596
9042
|
}
|
|
9597
9043
|
function buildAuditEntry(input, decision) {
|
|
9598
9044
|
return {
|
|
9599
|
-
id:
|
|
9045
|
+
id: randomUUID4(),
|
|
9600
9046
|
timestamp: new Date().toISOString(),
|
|
9601
9047
|
run_id: input.runId,
|
|
9602
9048
|
session_id: input.sessionID,
|
|
@@ -9629,11 +9075,6 @@ function createHarnessPolicy(directory, appLog) {
|
|
|
9629
9075
|
}
|
|
9630
9076
|
}
|
|
9631
9077
|
}
|
|
9632
|
-
function ensureBudget(runId) {
|
|
9633
|
-
if (!getBudget(runId)) {
|
|
9634
|
-
init(runId, config);
|
|
9635
|
-
}
|
|
9636
|
-
}
|
|
9637
9078
|
function checkRuntimeLimits(input) {
|
|
9638
9079
|
try {
|
|
9639
9080
|
const loop = loopDetector.checkBefore(input.tool, input.args, input.sessionID);
|
|
@@ -9643,11 +9084,6 @@ function createHarnessPolicy(directory, appLog) {
|
|
|
9643
9084
|
if (loop.action === "warn") {
|
|
9644
9085
|
return allow4(loop.message, "loop-detector", ["loop-warning"]);
|
|
9645
9086
|
}
|
|
9646
|
-
ensureBudget(input.runId);
|
|
9647
|
-
const spend = checkSpend(input.runId, input.tool);
|
|
9648
|
-
if (spend.verdict === "deny") {
|
|
9649
|
-
return deny3(spend.reason, "delegation-budget", spend.escalationMessage ?? "Tool call budget exhausted", ["budget-exhausted"]);
|
|
9650
|
-
}
|
|
9651
9087
|
if (isTraceStuck(input.directory, input.runId)) {
|
|
9652
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"]);
|
|
9653
9089
|
}
|
|
@@ -9826,7 +9262,7 @@ function createHarnessPolicy(directory, appLog) {
|
|
|
9826
9262
|
}
|
|
9827
9263
|
|
|
9828
9264
|
// src/services/harness-controller.ts
|
|
9829
|
-
import { randomUUID as
|
|
9265
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
9830
9266
|
|
|
9831
9267
|
// src/services/preflight-explorer.ts
|
|
9832
9268
|
var QUESTION_KIND_PATTERNS = [
|
|
@@ -10340,10 +9776,86 @@ function classifyTaskWithContext(description, exploration, sessionHistory = [])
|
|
|
10340
9776
|
};
|
|
10341
9777
|
}
|
|
10342
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
|
+
|
|
10343
9855
|
// src/services/workflow-scorecard.ts
|
|
10344
9856
|
import { existsSync as existsSync27, readFileSync as readFileSync27, appendFileSync as appendFileSync8, mkdirSync as mkdirSync17 } from "fs";
|
|
10345
9857
|
import { join as join27 } from "path";
|
|
10346
|
-
import { randomUUID as
|
|
9858
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
10347
9859
|
var DIMENSION_WEIGHTS = {
|
|
10348
9860
|
stageCompliance: 0.07,
|
|
10349
9861
|
designFirstCompliance: 0.1,
|
|
@@ -10408,7 +9920,7 @@ function generateScorecard(dir, trace, input = {}) {
|
|
|
10408
9920
|
const overallScore = Math.round(Object.entries(dimensions).reduce((sum, [key, val]) => sum + val * DIMENSION_WEIGHTS[key] * 100, 0));
|
|
10409
9921
|
const completion_status = trace.status === "complete" ? "complete" : trace.status === "failed" ? "failed" : trace.status === "cancelled" ? "cancelled" : "blocked";
|
|
10410
9922
|
const scorecard = {
|
|
10411
|
-
scorecard_id:
|
|
9923
|
+
scorecard_id: randomUUID6(),
|
|
10412
9924
|
run_id: trace.run_id,
|
|
10413
9925
|
session_id: trace.session_id,
|
|
10414
9926
|
command: trace.command,
|
|
@@ -10461,7 +9973,9 @@ function createStatePersistence(options) {
|
|
|
10461
9973
|
command,
|
|
10462
9974
|
rootSpanId: span.span_id,
|
|
10463
9975
|
currentSpanId: span.span_id,
|
|
10464
|
-
status: "running"
|
|
9976
|
+
status: "running",
|
|
9977
|
+
toolCallCount: 0,
|
|
9978
|
+
sameStepRetryCount: 0
|
|
10465
9979
|
};
|
|
10466
9980
|
contexts.set(sessionID, ctx);
|
|
10467
9981
|
return ctx;
|
|
@@ -10484,25 +9998,6 @@ function createStatePersistence(options) {
|
|
|
10484
9998
|
}
|
|
10485
9999
|
}
|
|
10486
10000
|
},
|
|
10487
|
-
openAgentSpan(ctx, parentSpanId, agent, task, stage3) {
|
|
10488
|
-
const parent = parentSpanId ? getSpan(directory, parentSpanId) : undefined;
|
|
10489
|
-
const depth = parent ? parent.depth + 1 : 0;
|
|
10490
|
-
const span = openSpan(directory, {
|
|
10491
|
-
trace_id: ctx.runId,
|
|
10492
|
-
invoker: ctx.command,
|
|
10493
|
-
agent,
|
|
10494
|
-
task_description: task,
|
|
10495
|
-
stage: stage3,
|
|
10496
|
-
parent_span_id: parentSpanId,
|
|
10497
|
-
depth
|
|
10498
|
-
});
|
|
10499
|
-
const updated = { ...ctx, currentSpanId: span.span_id };
|
|
10500
|
-
contexts.set(ctx.sessionID, updated);
|
|
10501
|
-
return span;
|
|
10502
|
-
},
|
|
10503
|
-
closeAgentSpan(spanId, status, opts = {}) {
|
|
10504
|
-
closeSpan(directory, spanId, status === "running" ? "failed" : status, opts);
|
|
10505
|
-
},
|
|
10506
10001
|
recordObservation(ctx, type, content) {
|
|
10507
10002
|
const list = observations.get(ctx.runId) ?? [];
|
|
10508
10003
|
list.push({
|
|
@@ -10523,8 +10018,8 @@ function createStatePersistence(options) {
|
|
|
10523
10018
|
|
|
10524
10019
|
// src/services/execution-substrate.ts
|
|
10525
10020
|
var executions = new Map;
|
|
10526
|
-
function makeKey(runId, sessionID,
|
|
10527
|
-
return `${runId}:${sessionID}:${
|
|
10021
|
+
function makeKey(runId, sessionID, tool13) {
|
|
10022
|
+
return `${runId}:${sessionID}:${tool13}`;
|
|
10528
10023
|
}
|
|
10529
10024
|
function createExecutionSubstrate() {
|
|
10530
10025
|
return {
|
|
@@ -10992,7 +10487,7 @@ function blockMessage(toolName) {
|
|
|
10992
10487
|
|
|
10993
10488
|
` + `Allowed tools for orchestrator: ${allowed.join(", ")}.
|
|
10994
10489
|
|
|
10995
|
-
` + `To
|
|
10490
|
+
` + `To route execution, mention the agent directly: @default-executor, @backend-coder, etc.
|
|
10996
10491
|
|
|
10997
10492
|
` + `To disable this guard: set FLOWDECK_ORCHESTRATOR_GUARD=off`;
|
|
10998
10493
|
}
|
|
@@ -11194,6 +10689,7 @@ function createVerificationLayer(directory) {
|
|
|
11194
10689
|
|
|
11195
10690
|
// src/services/recovery-layer.ts
|
|
11196
10691
|
var AUTO_STOP_TYPES = ["circular_delegation"];
|
|
10692
|
+
var retryCounts = new Map;
|
|
11197
10693
|
function signalToRecoveryAction(signal) {
|
|
11198
10694
|
switch (signal.recommended_action) {
|
|
11199
10695
|
case "stop":
|
|
@@ -11207,10 +10703,10 @@ function signalToRecoveryAction(signal) {
|
|
|
11207
10703
|
return "retry";
|
|
11208
10704
|
}
|
|
11209
10705
|
}
|
|
11210
|
-
function formatBudget(
|
|
11211
|
-
|
|
11212
|
-
|
|
11213
|
-
return `tool_calls=${
|
|
10706
|
+
function formatBudget(ctx) {
|
|
10707
|
+
const toolCalls = ctx.toolCallCount ?? 0;
|
|
10708
|
+
const retries = ctx.sameStepRetryCount ?? 0;
|
|
10709
|
+
return `tool_calls=${toolCalls}, retries=${retries}`;
|
|
11214
10710
|
}
|
|
11215
10711
|
function taskTypeToFailureTag(taskType) {
|
|
11216
10712
|
switch (taskType) {
|
|
@@ -11241,11 +10737,10 @@ function createRecoveryLayer(appLog) {
|
|
|
11241
10737
|
return isTraceStuck(dir, runId);
|
|
11242
10738
|
},
|
|
11243
10739
|
explainBlockage(ctx) {
|
|
11244
|
-
const budget = getBudget(ctx.runId);
|
|
11245
10740
|
const signals = detectDeadlocks(ctx.directory, ctx.runId);
|
|
11246
10741
|
const lines = [];
|
|
11247
10742
|
lines.push(`Run ${ctx.runId} (${ctx.command}) is blocked.`);
|
|
11248
|
-
lines.push(`Budget state: ${formatBudget(
|
|
10743
|
+
lines.push(`Budget state: ${formatBudget(ctx)}`);
|
|
11249
10744
|
if (signals.length === 0) {
|
|
11250
10745
|
lines.push("No deadlock signals detected.");
|
|
11251
10746
|
} else {
|
|
@@ -11258,14 +10753,11 @@ function createRecoveryLayer(appLog) {
|
|
|
11258
10753
|
`);
|
|
11259
10754
|
},
|
|
11260
10755
|
recommendRecovery(ctx, signals) {
|
|
11261
|
-
const budget = getBudget(ctx.runId);
|
|
11262
10756
|
if (signals.some((s) => s.auto_stop || AUTO_STOP_TYPES.includes(s.type))) {
|
|
11263
10757
|
return "stop";
|
|
11264
10758
|
}
|
|
11265
|
-
|
|
11266
|
-
|
|
11267
|
-
}
|
|
11268
|
-
if (budget && budget.sameStepRetries >= budget.maxSameStepRetries) {
|
|
10759
|
+
const maxRetries = 3;
|
|
10760
|
+
if ((ctx.sameStepRetryCount ?? 0) >= maxRetries) {
|
|
11269
10761
|
return "escalate";
|
|
11270
10762
|
}
|
|
11271
10763
|
const severityOrder = ["stop", "escalate", "fallback-agent", "retry"];
|
|
@@ -11277,7 +10769,9 @@ function createRecoveryLayer(appLog) {
|
|
|
11277
10769
|
return "retry";
|
|
11278
10770
|
},
|
|
11279
10771
|
canRetry(runId) {
|
|
11280
|
-
|
|
10772
|
+
const current = retryCounts.get(runId) ?? 0;
|
|
10773
|
+
retryCounts.set(runId, current + 1);
|
|
10774
|
+
return current < 3;
|
|
11281
10775
|
},
|
|
11282
10776
|
getRelevantFailures(dir, taskType) {
|
|
11283
10777
|
try {
|
|
@@ -11358,8 +10852,8 @@ function createToolOutcomeHandler(deps) {
|
|
|
11358
10852
|
executionSubstrate,
|
|
11359
10853
|
state
|
|
11360
10854
|
} = deps;
|
|
11361
|
-
function getLastAuditEntryId(runId,
|
|
11362
|
-
const entries = auditLog.query({ run_id: runId, tool:
|
|
10855
|
+
function getLastAuditEntryId(runId, tool13) {
|
|
10856
|
+
const entries = auditLog.query({ run_id: runId, tool: tool13 });
|
|
11363
10857
|
return entries.at(-1)?.id;
|
|
11364
10858
|
}
|
|
11365
10859
|
return (req, output, status) => {
|
|
@@ -11507,8 +11001,8 @@ function createHarnessController(config) {
|
|
|
11507
11001
|
const orchestratorGuard = new OrchestratorGuard;
|
|
11508
11002
|
orchestratorGuard.setPolicy(policy);
|
|
11509
11003
|
const loopDetector = lazy(() => {
|
|
11510
|
-
const
|
|
11511
|
-
const loopCfg =
|
|
11004
|
+
const flowdeckConfig2 = loadFlowDeckConfig(directory);
|
|
11005
|
+
const loopCfg = flowdeckConfig2.governance?.loopDetection ?? {};
|
|
11512
11006
|
return new LoopDetector({
|
|
11513
11007
|
enabled: loopCfg.enabled ?? true,
|
|
11514
11008
|
maxRepeats: loopCfg.maxRepeats ?? 2,
|
|
@@ -11522,6 +11016,12 @@ function createHarnessController(config) {
|
|
|
11522
11016
|
const contextMonitor = lazy(() => config.contextMonitor ?? createContextWindowMonitorHook({
|
|
11523
11017
|
getTotalBudget: (sessionID) => contextIngress.getTotalBudget(sessionID)
|
|
11524
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;
|
|
11525
11025
|
const state = {
|
|
11526
11026
|
loopDetector,
|
|
11527
11027
|
eventLog,
|
|
@@ -11578,7 +11078,7 @@ function createHarnessController(config) {
|
|
|
11578
11078
|
};
|
|
11579
11079
|
function buildCommandAuditEntry(req, runCtx) {
|
|
11580
11080
|
return {
|
|
11581
|
-
id:
|
|
11081
|
+
id: randomUUID7(),
|
|
11582
11082
|
timestamp: new Date().toISOString(),
|
|
11583
11083
|
run_id: runCtx.runId,
|
|
11584
11084
|
session_id: req.sessionID,
|
|
@@ -11699,10 +11199,52 @@ function createHarnessController(config) {
|
|
|
11699
11199
|
recordToolOutcome(req, output, status) {
|
|
11700
11200
|
toolOutcomeHandler(req, output, status);
|
|
11701
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
|
+
},
|
|
11702
11222
|
async onSessionEvent(evt) {
|
|
11703
11223
|
const type = evt.type;
|
|
11704
11224
|
const properties = evt.properties ?? {};
|
|
11705
|
-
const 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
|
+
}
|
|
11706
11248
|
orchestratorGuard.onEvent({ type, properties });
|
|
11707
11249
|
if (type === "session.created" || type === "session.started") {
|
|
11708
11250
|
const healthy = await eventLog.get().session({ directory }, { type, properties });
|
|
@@ -11749,6 +11291,30 @@ function createHarnessController(config) {
|
|
|
11749
11291
|
}
|
|
11750
11292
|
};
|
|
11751
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
|
+
}
|
|
11752
11318
|
|
|
11753
11319
|
// src/index.ts
|
|
11754
11320
|
function lazyLoadRulePaths(projectRoot) {
|
|
@@ -11808,7 +11374,6 @@ var plugin = async (input, _options) => {
|
|
|
11808
11374
|
contextIngress
|
|
11809
11375
|
});
|
|
11810
11376
|
harness.init();
|
|
11811
|
-
const delegateTool = createDelegateTool(client, harness.getStatePersistence(), (input2) => harness.ensureRunContext(input2));
|
|
11812
11377
|
const contextMonitor = harness.getContextMonitor();
|
|
11813
11378
|
const shellEnvHook = createShellEnvHook({ directory, worktree });
|
|
11814
11379
|
const todoHook = createTodoHook(client);
|
|
@@ -11914,8 +11479,7 @@ var plugin = async (input, _options) => {
|
|
|
11914
11479
|
codegraph: codegraphTool,
|
|
11915
11480
|
"load-rules": loadRulesTool,
|
|
11916
11481
|
"list-rules": listRulesTool,
|
|
11917
|
-
"merge-assist": mergeAssistTool
|
|
11918
|
-
delegate: delegateTool
|
|
11482
|
+
"merge-assist": mergeAssistTool
|
|
11919
11483
|
},
|
|
11920
11484
|
"shell.env": shellEnvHook,
|
|
11921
11485
|
"todo.updated": todoHook,
|
|
@@ -11968,6 +11532,11 @@ var plugin = async (input, _options) => {
|
|
|
11968
11532
|
}
|
|
11969
11533
|
},
|
|
11970
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
|
+
}
|
|
11971
11540
|
if ((toolInput.tool === "read" || toolInput.tool === "view") && toolOutput?.args) {
|
|
11972
11541
|
if (typeof toolOutput.args.offset === "string") {
|
|
11973
11542
|
const n = Number(toolOutput.args.offset);
|
|
@@ -11979,7 +11548,7 @@ var plugin = async (input, _options) => {
|
|
|
11979
11548
|
}
|
|
11980
11549
|
}
|
|
11981
11550
|
const decision = harness.evaluateToolCall({
|
|
11982
|
-
sessionID
|
|
11551
|
+
sessionID,
|
|
11983
11552
|
tool: toolInput.tool ?? toolInput.name ?? "unknown",
|
|
11984
11553
|
args: toolOutput?.args ?? toolInput?.args ?? {},
|
|
11985
11554
|
agent: toolInput.agent
|