@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/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 -710
- 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 -32
- 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 -55
- 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,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(
|
|
6331
|
-
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");
|
|
6332
5868
|
}
|
|
6333
5869
|
|
|
6334
5870
|
// src/hooks/shell-env-hook.ts
|
|
6335
|
-
import { existsSync as
|
|
6336
|
-
import { join as
|
|
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 (
|
|
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) &&
|
|
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 =
|
|
6390
|
-
if (!
|
|
5925
|
+
const statePath3 = join16(root, ".planning", "STATE.md");
|
|
5926
|
+
if (!existsSync16(statePath3))
|
|
6391
5927
|
return;
|
|
6392
5928
|
try {
|
|
6393
|
-
const content =
|
|
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
|
|
6499
|
-
import { join as
|
|
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 (!
|
|
6076
|
+
if (!existsSync17(sp))
|
|
6541
6077
|
return null;
|
|
6542
6078
|
try {
|
|
6543
|
-
const content =
|
|
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 =
|
|
6581
|
-
if (indexChanged &&
|
|
6116
|
+
const indexPath2 = join17(ctx.directory, ".planning", "CODEBASE_INDEX.md");
|
|
6117
|
+
if (indexChanged && existsSync17(indexPath2)) {
|
|
6582
6118
|
try {
|
|
6583
|
-
const indexContent =
|
|
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 (
|
|
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
|
|
6766
|
-
import { join as
|
|
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 ?
|
|
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(
|
|
6311
|
+
candidates.push(join18(directory, ".opencode", CONFIG_FILENAME));
|
|
6776
6312
|
}
|
|
6777
|
-
candidates.push(
|
|
6313
|
+
candidates.push(join18(getGlobalConfigDir(), CONFIG_FILENAME));
|
|
6778
6314
|
for (const configPath of candidates) {
|
|
6779
|
-
if (
|
|
6315
|
+
if (existsSync18(configPath)) {
|
|
6780
6316
|
try {
|
|
6781
|
-
const content =
|
|
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
|
|
6805
|
-
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";
|
|
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 =
|
|
6931
|
-
if (
|
|
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 (
|
|
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(
|
|
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 (
|
|
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 =
|
|
7165
|
-
if (!
|
|
7166
|
-
|
|
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
|
-
|
|
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 =
|
|
7283
|
-
const planPath =
|
|
7284
|
-
if (!
|
|
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 =
|
|
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 =
|
|
7300
|
-
if (!
|
|
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 =
|
|
6843
|
+
const filePath = join19(codebaseDir2, file);
|
|
7308
6844
|
try {
|
|
7309
|
-
docs[file] =
|
|
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 =
|
|
7317
|
-
if (!
|
|
6852
|
+
const eventsPath = join19(projectRoot, ".opencode", "flowdeck-events.jsonl");
|
|
6853
|
+
if (!existsSync19(eventsPath))
|
|
7318
6854
|
return [];
|
|
7319
6855
|
try {
|
|
7320
|
-
const content =
|
|
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 =
|
|
7382
|
-
|
|
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 =
|
|
7409
|
-
if (!
|
|
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
|
|
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
|
|
7508
|
-
if (
|
|
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 (
|
|
7061
|
+
if (tool13 === "read" || tool13 === "view") {
|
|
7514
7062
|
const filePath = typeof args.filePath === "string" ? args.filePath : "";
|
|
7515
7063
|
try {
|
|
7516
|
-
return `${
|
|
7064
|
+
return `${tool13}:${resolve2(filePath || "")}`;
|
|
7517
7065
|
} catch {
|
|
7518
|
-
return `${
|
|
7066
|
+
return `${tool13}:${filePath}`;
|
|
7519
7067
|
}
|
|
7520
7068
|
}
|
|
7521
|
-
if (
|
|
7069
|
+
if (tool13 === "write" || tool13 === "edit") {
|
|
7522
7070
|
const filePath = typeof args.filePath === "string" ? args.filePath : "";
|
|
7523
7071
|
try {
|
|
7524
|
-
return `${
|
|
7072
|
+
return `${tool13}:${resolve2(filePath || "")}`;
|
|
7525
7073
|
} catch {
|
|
7526
|
-
return `${
|
|
7074
|
+
return `${tool13}:${filePath}`;
|
|
7527
7075
|
}
|
|
7528
7076
|
}
|
|
7529
|
-
if (
|
|
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 `${
|
|
7080
|
+
return `${tool13}:${pattern}:${resolve2(path || ".")}`;
|
|
7533
7081
|
}
|
|
7534
7082
|
const sorted = stableStringify(args);
|
|
7535
|
-
return `${
|
|
7083
|
+
return `${tool13}:${sorted}`;
|
|
7536
7084
|
}
|
|
7537
7085
|
function classifyObservation(toolName, previous, output, status, similarityThreshold) {
|
|
7538
7086
|
const outputPreview = getOutputPreview(output);
|
|
7539
|
-
const
|
|
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 (
|
|
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(
|
|
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
|
|
7574
|
-
if (
|
|
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 `${
|
|
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 `${
|
|
7133
|
+
return `${tool13}:"${body}"`;
|
|
7586
7134
|
}
|
|
7587
7135
|
const preview = body.slice(0, 30);
|
|
7588
7136
|
const hash = djb2Hash(body);
|
|
7589
|
-
return `${
|
|
7137
|
+
return `${tool13}:"${preview}" (hash: ${hash})`;
|
|
7590
7138
|
}
|
|
7591
|
-
return `${
|
|
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
|
|
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 (
|
|
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
|
|
8523
|
-
import { join as
|
|
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(
|
|
8636
|
-
const patterns = BLOCKED_PATTERNS[
|
|
8182
|
+
function isBlocked(tool13, args) {
|
|
8183
|
+
const patterns = BLOCKED_PATTERNS[tool13];
|
|
8637
8184
|
if (!patterns)
|
|
8638
8185
|
return null;
|
|
8639
|
-
if (
|
|
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 (
|
|
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
|
|
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 =
|
|
8660
|
-
if (!
|
|
8206
|
+
const constraintsPath = join20(codebaseDir(directory), "CONSTRAINTS.md");
|
|
8207
|
+
if (!existsSync20(constraintsPath))
|
|
8661
8208
|
return null;
|
|
8662
8209
|
try {
|
|
8663
|
-
const content =
|
|
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 (!
|
|
8251
|
+
if (!existsSync20(planPath))
|
|
8705
8252
|
return false;
|
|
8706
|
-
const planContent =
|
|
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:
|
|
8725
|
-
if (
|
|
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(
|
|
8275
|
+
const blocked = isBlocked(tool13, args);
|
|
8729
8276
|
if (blocked) {
|
|
8730
8277
|
return deny(blocked, blocked, ["dangerous-pattern"]);
|
|
8731
8278
|
}
|
|
8732
|
-
if (
|
|
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
|
|
8752
|
-
import { join as
|
|
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 (
|
|
8304
|
+
if (existsSync21(configPath)) {
|
|
8758
8305
|
try {
|
|
8759
|
-
const config = JSON.parse(
|
|
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:
|
|
8822
|
-
const planningDirPath =
|
|
8368
|
+
const { directory, tool: tool13, args } = input;
|
|
8369
|
+
const planningDirPath = join21(directory, PLANNING_DIR2);
|
|
8823
8370
|
const codebaseDirectory = codebaseDir(directory);
|
|
8824
|
-
const configPath =
|
|
8825
|
-
const statePath3 =
|
|
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" && !
|
|
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 (
|
|
8835
|
-
if (!
|
|
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 (!
|
|
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 (
|
|
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 (!
|
|
8449
|
+
if (!existsSync21(planPath))
|
|
8903
8450
|
return false;
|
|
8904
|
-
const planContent =
|
|
8451
|
+
const planContent = readFileSync21(planPath, "utf-8");
|
|
8905
8452
|
return isUiHeavyTask(planContent);
|
|
8906
8453
|
}
|
|
8907
8454
|
function effectiveSeverity(configPath, statePath3) {
|
|
8908
|
-
if (
|
|
8455
|
+
if (existsSync21(configPath)) {
|
|
8909
8456
|
try {
|
|
8910
|
-
const configContent =
|
|
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 (!
|
|
8470
|
+
if (!existsSync21(statePath3))
|
|
8924
8471
|
return false;
|
|
8925
8472
|
try {
|
|
8926
|
-
const content =
|
|
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 (!
|
|
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 (!
|
|
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
|
|
8948
|
-
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";
|
|
8949
8496
|
import { createHash as createHash4 } from "crypto";
|
|
8950
|
-
import { randomUUID
|
|
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
|
|
8534
|
+
return join22(codebaseDir(dir), "APPROVALS.json");
|
|
8988
8535
|
}
|
|
8989
8536
|
function loadStore(dir) {
|
|
8990
8537
|
const p = approvalsPath(dir);
|
|
8991
|
-
if (!
|
|
8538
|
+
if (!existsSync22(p))
|
|
8992
8539
|
return { requests: [] };
|
|
8993
8540
|
try {
|
|
8994
|
-
return JSON.parse(
|
|
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 (!
|
|
9002
|
-
|
|
9003
|
-
|
|
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:
|
|
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,
|
|
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,
|
|
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 "${
|
|
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:
|
|
9074
|
-
if (!WRITE_TOOLS.has(
|
|
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
|
|
9099
|
-
import { join as
|
|
9100
|
-
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";
|
|
9101
8648
|
|
|
9102
8649
|
// src/services/agent-trace-graph.ts
|
|
9103
|
-
import { existsSync as
|
|
9104
|
-
import { join as
|
|
9105
|
-
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";
|
|
9106
8653
|
function agentSpansPath(dir) {
|
|
9107
|
-
return
|
|
8654
|
+
return join23(codebaseDir(dir), "AGENT_SPANS.jsonl");
|
|
9108
8655
|
}
|
|
9109
8656
|
function loadAllSpans(dir) {
|
|
9110
8657
|
const p = agentSpansPath(dir);
|
|
9111
|
-
if (!
|
|
8658
|
+
if (!existsSync23(p))
|
|
9112
8659
|
return [];
|
|
9113
8660
|
try {
|
|
9114
|
-
return
|
|
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 (!
|
|
9124
|
-
|
|
9125
|
-
|
|
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 (!
|
|
9132
|
-
|
|
8678
|
+
if (!existsSync23(cd))
|
|
8679
|
+
mkdirSync13(cd, { recursive: true });
|
|
9133
8680
|
const span = {
|
|
9134
|
-
span_id:
|
|
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
|
-
|
|
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
|
|
8763
|
+
return join24(codebaseDir(dir), "DEADLOCK_SIGNALS.jsonl");
|
|
9220
8764
|
}
|
|
9221
8765
|
function appendSignal(dir, signal) {
|
|
9222
8766
|
const cd = codebaseDir(dir);
|
|
9223
|
-
if (!
|
|
9224
|
-
|
|
9225
|
-
|
|
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 (!
|
|
8774
|
+
if (!existsSync24(p))
|
|
9231
8775
|
return [];
|
|
9232
8776
|
try {
|
|
9233
|
-
const all =
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
9381
|
-
mkdirSync as
|
|
9382
|
-
appendFileSync as
|
|
9383
|
-
readFileSync as
|
|
9384
|
-
writeFileSync as
|
|
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
|
|
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
|
|
8936
|
+
return join25(codebaseDir(directory), AUDIT_FILE);
|
|
9393
8937
|
}
|
|
9394
8938
|
function ensureDirectory(dir) {
|
|
9395
8939
|
const base = codebaseDir(dir);
|
|
9396
|
-
if (!
|
|
9397
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
8976
|
+
if (!existsSync25(path))
|
|
9433
8977
|
return [];
|
|
9434
8978
|
try {
|
|
9435
|
-
const lines =
|
|
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:
|
|
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
|
|
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
|
|
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:
|
|
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,
|
|
10429
|
-
return `${runId}:${sessionID}:${
|
|
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
|
|
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(
|
|
11113
|
-
|
|
11114
|
-
|
|
11115
|
-
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}`;
|
|
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(
|
|
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
|
-
|
|
11168
|
-
|
|
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
|
-
|
|
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,
|
|
11264
|
-
const entries = auditLog.query({ run_id: runId, tool:
|
|
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
|
|
11413
|
-
const loopCfg =
|
|
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:
|
|
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 =
|
|
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
|
|
11551
|
+
sessionID,
|
|
11885
11552
|
tool: toolInput.tool ?? toolInput.name ?? "unknown",
|
|
11886
11553
|
args: toolOutput?.args ?? toolInput?.args ?? {},
|
|
11887
11554
|
agent: toolInput.agent
|