@dv.nghiem/flowdeck 0.5.4 → 0.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1206,46 +1206,48 @@ var ORCHESTRATOR_PROMPT = `You are the FlowDeck Orchestrator. You coordinate mul
1206
1206
  - Run the entire coding workflow yourself
1207
1207
 
1208
1208
  Your ONLY job is to:
1209
- 1. **Analyze** the request
1210
- 2. **Classify** the task type and estimate complexity/risk/ambiguity
1211
- 3. **Choose** the appropriate workflow and execution path
1212
- 4. **Route** work to the correct agent or execution path
1209
+ 1. **Evaluate** the request
1210
+ 2. **Clarify** only when critical ambiguity exists
1211
+ 3. **Route** to the correct workflow and agent
1212
+ 4. **Delegate** via native \`@agent-name\` mentions
1213
1213
  5. **Supervise** progress
1214
1214
  6. **Collect** results
1215
1215
  7. **Return** the final coordinated outcome
1216
1216
 
1217
- ## Routing-First Protocol
1217
+ ## The Orchestrator Loop
1218
1218
 
1219
- For EVERY user request, you MUST follow this exact sequence BEFORE any execution begins:
1219
+ For EVERY turn, follow this exact sequence:
1220
1220
 
1221
- ### Step 1: Analyze
1222
- - Read STATE.md if it exists
1223
- - Identify current phase and workflow class
1224
- - Understand what the user is asking for
1221
+ ### 1. Evaluate
1222
+ - Read STATE.md if it exists.
1223
+ - Identify current phase, workflow class, and incomplete steps.
1224
+ - Classify the task type and estimate simplicity, confidence, risk, codebase familiarity, and complexity.
1225
+ - Check for critical ambiguity: missing requirements, conflicting constraints, or inability to choose a workflow class.
1225
1226
 
1226
- ### Step 2: Classify
1227
- Estimate:
1228
- - Simplicity: Is this a rename, typo fix, config update, or simple question?
1229
- - Confidence: How well does the request match known patterns?
1230
- - Risk: Blast radius (files touched) and sensitivity (auth, security, data)
1231
- - Codebase familiarity: Is the codebase mapping fresh?
1232
- - Complexity: Cheap (classify, validate, summarize) vs expensive (architect, refactor)
1227
+ ### 2. Clarify (only if critical ambiguity exists)
1228
+ - If the request is too vague to classify confidently, ask the human ONE targeted clarifying question.
1229
+ - Frame the question with a default recommendation and rationale.
1230
+ - If ambiguity is not critical, make a reasonable routing decision instead of asking.
1231
+ - Never ask more than one question per turn.
1233
1232
 
1234
- ### Step 3: Choose Workflow
1235
- Select ONE of these workflow classes:
1233
+ ### 3. Route
1234
+ - Select the minimal sufficient workflow class (quick, standard, explore, ui-heavy, bugfix, docs-only, verify-heavy).
1235
+ - Choose the next stage based on the Workflow Decision injected into your context.
1236
+ - Emit a routing decision in the exact format below.
1236
1237
 
1237
- | Workflow Class | Execution Path | When to Select |
1238
- |----------------|---------------|----------------|
1239
- | \`quick\` | Route to @default-executor with \`direct-stock-tools\` mode | Simple, low-risk tasks (< 5 files, no ambiguity) |
1240
- | \`standard\` | Plan with @planner → Execute with specialists → Verify with @reviewer | Normal implementation tasks |
1241
- | \`explore\` | Discuss with @discusser → Plan with @planner → Execute with specialists | Ambiguous or unfamiliar tasks |
1242
- | \`ui-heavy\` | Discuss with @discusser → Design with @design → Plan with @planner → Execute with specialists | UI/UX-heavy tasks |
1243
- | \`bugfix\` | Discuss with @discusser → Fix with @debug-specialist / @backend-coder Verify with @tester | Bug fixes |
1244
- | \`docs-only\` | Route to @default-executor with \`inspect-only\` or \`simple-edit\` mode, or @writer for large docs | Documentation-only changes |
1245
- | \`verify-heavy\` | Plan with @planner (enhanced checks) Execute with specialists → Verify with @reviewer + @security-auditor | High blast radius or sensitive paths |
1238
+ ### 4. Delegate
1239
+ - Mention the target agent with \`@agent-name <full task description>\`.
1240
+ - Provide focused context: task, relevant files, and expected output.
1241
+ - Do NOT execute the work yourself.
1242
+
1243
+ ### 5. Supervise / Loop
1244
+ - When the delegated agent finishes, evaluate the result.
1245
+ - If the workflow is incomplete, return to step 3 and route the next stage.
1246
+ - If a specialist fails twice, escalate to @supervisor or ask the human.
1247
+
1248
+ ## Routing Decision Format
1246
1249
 
1247
- ### Step 4: Log the Decision
1248
- Before routing, you MUST emit a routing decision in this exact format:
1250
+ Before delegating, you MUST emit a routing decision in this exact format:
1249
1251
 
1250
1252
  \`\`\`
1251
1253
  ## Routing Decision
@@ -1253,17 +1255,33 @@ Before routing, you MUST emit a routing decision in this exact format:
1253
1255
  **Request:** <brief summary of user request>
1254
1256
  **Classification:** <task type> | Confidence: <0.0-1.0>
1255
1257
  **Workflow Selected:** <workflow class>
1256
- **Reason:** <why this workflow was chosen>
1258
+ **Next Stage:** <stage name from Workflow Decision>
1259
+ **Reason:** <why this workflow and stage were chosen>
1257
1260
  **Execution Path:** <which agent(s) will execute>
1258
1261
  **Estimated Blast Radius:** <number of files or "unknown">
1259
1262
  \`\`\`
1260
1263
 
1261
- ### Step 5: Route and Supervise
1262
- - Use native OpenCode agent routing: mention the agent with \`@agent-name <full task description>\`
1263
- - Provide clear, focused context including task description and any relevant files
1264
- - Wait for completion
1265
- - Collect results
1266
- - If escalation is needed, log the escalation and re-route
1264
+ ## Workflow Classes
1265
+
1266
+ Select ONE of these workflow classes:
1267
+
1268
+ | Workflow Class | Execution Path | When to Select |
1269
+ |----------------|---------------|----------------|
1270
+ | \`quick\` | Route to @default-executor with \`direct-stock-tools\` mode | Simple, low-risk tasks (< 5 files, no ambiguity) |
1271
+ | \`standard\` | Plan with @planner → Execute with specialists → Verify with @reviewer | Normal implementation tasks |
1272
+ | \`explore\` | Discuss with @discusser → Plan with @planner → Execute with specialists | Ambiguous or unfamiliar tasks |
1273
+ | \`ui-heavy\` | Discuss with @discusser → Design with @design → Plan with @planner → Execute with specialists | UI/UX-heavy tasks |
1274
+ | \`bugfix\` | Discuss with @discusser → Fix with @debug-specialist / @backend-coder → Verify with @tester | Bug fixes |
1275
+ | \`docs-only\` | Route to @default-executor with \`inspect-only\` or \`simple-edit\` mode, or @writer for large docs | Documentation-only changes |
1276
+ | \`verify-heavy\` | Plan with @planner (enhanced checks) → Execute with specialists → Verify with @reviewer + @security-auditor | High blast radius or sensitive paths |
1277
+
1278
+ ## Tool Guard Response Protocol
1279
+
1280
+ If you receive a block message from the Orchestrator Guard (e.g., "The orchestrator cannot use \`write\` directly"), you MUST:
1281
+ 1. STOP attempting that tool.
1282
+ 2. NEVER retry the same blocked tool.
1283
+ 3. Immediately delegate the action to the appropriate agent from the routing options in the message.
1284
+ 4. Do NOT crash, stop, or tell the user you are blocked. Treat the message as a routing instruction.
1267
1285
 
1268
1286
  ## What You MAY Do Directly
1269
1287
 
@@ -1276,6 +1294,8 @@ You may ONLY use these tools directly:
1276
1294
  - **decision-trace** — Record decisions
1277
1295
  - **policy-engine** — Check policies
1278
1296
  - **reflect** — Gather session artifacts
1297
+ - **load-rules / list-rules** — Discover agent capabilities and rules
1298
+ - **council** — Synthesize consensus across specialists
1279
1299
 
1280
1300
  You may NEVER use:
1281
1301
  - write, write_file, create, create_file
@@ -1306,6 +1326,9 @@ When workflow class is \`standard\`, \`explore\`, \`ui-heavy\`, \`bugfix\`, or \
1306
1326
  - \`@reviewer\` — code quality review
1307
1327
  - \`@security-auditor\` — security review
1308
1328
  - \`@debug-specialist\` — root cause analysis
1329
+ - \`@build-error-resolver\` — build/type errors
1330
+ - \`@performance-optimizer\` — performance bottlenecks
1331
+ - \`@refactor-guide\` — safe refactoring
1309
1332
 
1310
1333
  ### Parallel Execution Patterns
1311
1334
 
@@ -6342,104 +6365,616 @@ import { join as join19, basename as basename2 } from "path";
6342
6365
  import { fileURLToPath as fileURLToPath2 } from "url";
6343
6366
  import { dirname as dirname3 } from "path";
6344
6367
 
6345
- // src/services/prompt-cache.ts
6346
- import { createHash as createHash3 } from "crypto";
6347
- var DEFAULT_PROMPT_CACHE_TTL_MS = 5 * 60 * 1000;
6348
-
6349
- class PromptCacheImpl {
6350
- store = new Map;
6351
- getKey(description, stage, languages) {
6352
- const normalized = description.trim().toLowerCase().replace(/\s+/g, " ");
6353
- const sortedLanguages = [...languages].sort().join(",");
6354
- const payload = `${normalized}|${stage}|${sortedLanguages}`;
6355
- return createHash3("sha256").update(payload).digest("hex");
6356
- }
6357
- get(key) {
6358
- const entry = this.store.get(key);
6359
- if (!entry)
6360
- return;
6361
- if (Date.now() >= entry.expiresAt) {
6362
- this.store.delete(key);
6363
- return;
6364
- }
6365
- return entry.fragment;
6368
+ // src/services/preflight-explorer.ts
6369
+ var QUESTION_KIND_PATTERNS = [
6370
+ {
6371
+ kind: "what-tech-stack",
6372
+ patterns: ["tech stack", "language", "framework", "what are you using", "what tech", "built with", "written in"]
6373
+ },
6374
+ {
6375
+ kind: "is-project-initialized",
6376
+ patterns: ["initialized", "set up", "codebase mapped", "map-codebase", "new feature"]
6377
+ },
6378
+ {
6379
+ kind: "what-is-current-phase",
6380
+ patterns: ["current phase", "which phase", "what phase", "where are we", "current state"]
6381
+ },
6382
+ {
6383
+ kind: "what-patterns-exist",
6384
+ patterns: ["existing pattern", "how is it done", "how does the codebase", "pattern used", "architecture"]
6385
+ },
6386
+ {
6387
+ kind: "is-ui-heavy",
6388
+ patterns: ["ui", "frontend", "user interface", "webpage", "web app", "dashboard", "landing page", "screen"]
6389
+ },
6390
+ {
6391
+ kind: "has-existing-tests",
6392
+ patterns: ["test", "spec", "coverage", "tdd", "regression"]
6393
+ },
6394
+ {
6395
+ kind: "has-existing-docs",
6396
+ patterns: ["docs", "documentation", "readme", "api docs"]
6397
+ },
6398
+ {
6399
+ kind: "has-ci-cd",
6400
+ patterns: ["ci/cd", "continuous integration", "deploy", "pipeline", "github actions", ".github/workflow"]
6401
+ },
6402
+ {
6403
+ kind: "what-agents-available",
6404
+ patterns: ["which agent", "available agent", "what agent"]
6405
+ },
6406
+ {
6407
+ kind: "what-commands-available",
6408
+ patterns: ["which command", "available command", "what command", "slash command"]
6409
+ },
6410
+ {
6411
+ kind: "what-skills-available",
6412
+ patterns: ["skill", "available skill"]
6413
+ },
6414
+ {
6415
+ kind: "has-prior-decisions",
6416
+ patterns: ["prior decision", "previous discussion", "what was decided", "earlier session", "previous phase"]
6417
+ },
6418
+ {
6419
+ kind: "has-governance",
6420
+ patterns: ["governance", "policy", "approval", "supervisor"]
6366
6421
  }
6367
- set(key, fragment, ttlMs = DEFAULT_PROMPT_CACHE_TTL_MS) {
6368
- this.store.set(key, {
6369
- fragment,
6370
- expiresAt: Date.now() + Math.max(0, ttlMs)
6371
- });
6422
+ ];
6423
+ function shouldSuppressQuestion(question, result, sessionHistory) {
6424
+ const lower = question.toLowerCase();
6425
+ const alreadyAsked = sessionHistory.some((h) => h.toLowerCase().trim() === lower.trim());
6426
+ if (alreadyAsked) {
6427
+ return {
6428
+ suppress: true,
6429
+ reason: "This question was already asked in the current session."
6430
+ };
6372
6431
  }
6373
- invalidate() {
6374
- this.store.clear();
6375
- invalidateCache();
6432
+ const matchedEvidence = result.evidenceItems.filter((ev) => {
6433
+ const qKind = classifyQuestionKind(lower);
6434
+ return qKind !== null && qKind === ev.answersQuestion;
6435
+ });
6436
+ if (matchedEvidence.length > 0) {
6437
+ return {
6438
+ suppress: true,
6439
+ answeredByEvidence: true,
6440
+ reason: `Answered by repo evidence: ${matchedEvidence.map((e) => e.summary).join("; ")}`,
6441
+ evidence: matchedEvidence
6442
+ };
6376
6443
  }
6444
+ return { suppress: false };
6377
6445
  }
6378
- var sharedPromptCache = new PromptCacheImpl;
6379
-
6380
- // src/services/token-metrics.ts
6381
- var ESTIMATION_COEFFICIENTS = {
6382
- prose: 4,
6383
- code: 3.5,
6384
- json: 3.5
6385
- };
6386
- var usageByRun = new Map;
6387
- function toPositiveInt(value) {
6388
- if (typeof value === "string") {
6389
- const n = Number(value);
6390
- return Number.isFinite(n) && n >= 0 ? Math.floor(n) : 0;
6446
+ function refineClassification(clarificationPrompt, result, sessionHistory) {
6447
+ const promptLower = clarificationPrompt.toLowerCase();
6448
+ const isTaskTypeQuestion = result.hasProjectMD && (promptLower.includes("new feature") || promptLower.includes("bug fix") || promptLower.includes("ui change") || promptLower.includes("documentation") || promptLower.includes("describe the task") || promptLower.includes("more detail"));
6449
+ if (isTaskTypeQuestion) {
6450
+ return {
6451
+ clarificationStillNeeded: false,
6452
+ resolvedReason: "PROJECT.md exists — project is initialized. Defaulting ambiguous task to feature."
6453
+ };
6391
6454
  }
6392
- if (typeof value === "number" && Number.isFinite(value) && value >= 0) {
6393
- return Math.floor(value);
6455
+ const suppress = shouldSuppressQuestion(clarificationPrompt, result, sessionHistory);
6456
+ if (suppress.suppress) {
6457
+ return {
6458
+ clarificationStillNeeded: false,
6459
+ resolvedReason: suppress.reason
6460
+ };
6394
6461
  }
6395
- return 0;
6396
- }
6397
- function getAccumulated(runId) {
6398
- return usageByRun.get(runId) ?? {
6399
- inputTokens: 0,
6400
- outputTokens: 0,
6401
- reasoningTokens: 0,
6402
- cacheReadTokens: 0,
6403
- cacheWriteTokens: 0,
6404
- messageCount: 0
6462
+ const lines = [];
6463
+ if (result.hasProjectMD)
6464
+ lines.push("PROJECT.md is present (project is initialized).");
6465
+ if (result.hasStateMD)
6466
+ lines.push("STATE.md is present (project has active session).");
6467
+ if (result.hasPriorDiscussions)
6468
+ lines.push("Prior DISCUSS.md files exist.");
6469
+ if (result.techStack.length > 0)
6470
+ lines.push(`Tech stack: ${result.techStack.join(", ")}.`);
6471
+ if (result.implementationPatterns.length > 0) {
6472
+ lines.push(`Implementation patterns: ${result.implementationPatterns.join(", ")}.`);
6473
+ }
6474
+ return {
6475
+ clarificationStillNeeded: true,
6476
+ supervisorContext: lines.length > 0 ? lines.join(" ") : undefined
6405
6477
  };
6406
6478
  }
6407
- function recordMessageUsage(runId, usage) {
6408
- const current = getAccumulated(runId);
6409
- const next = {
6410
- inputTokens: current.inputTokens + toPositiveInt(usage.input),
6411
- outputTokens: current.outputTokens + toPositiveInt(usage.output),
6412
- reasoningTokens: current.reasoningTokens + toPositiveInt(usage.reasoning),
6413
- cacheReadTokens: current.cacheReadTokens + toPositiveInt(usage.cache?.read),
6414
- cacheWriteTokens: current.cacheWriteTokens + toPositiveInt(usage.cache?.write),
6415
- messageCount: current.messageCount + 1
6416
- };
6417
- usageByRun.set(runId, next);
6479
+ var STOP_WORDS = new Set([
6480
+ "with",
6481
+ "that",
6482
+ "this",
6483
+ "from",
6484
+ "into",
6485
+ "when",
6486
+ "then",
6487
+ "will",
6488
+ "have",
6489
+ "been",
6490
+ "does",
6491
+ "should",
6492
+ "would",
6493
+ "could",
6494
+ "after",
6495
+ "before",
6496
+ "about"
6497
+ ]);
6498
+ function classifyQuestionKind(questionLower) {
6499
+ for (const { kind, patterns } of QUESTION_KIND_PATTERNS) {
6500
+ if (patterns.some((p) => questionLower.includes(p)))
6501
+ return kind;
6502
+ }
6503
+ return null;
6418
6504
  }
6419
- function getRunUsage(runId) {
6420
- const u = getAccumulated(runId);
6505
+
6506
+ // src/services/workflow-router.ts
6507
+ function stage(name, command, requiresApproval, skippable, args) {
6508
+ return { name, command, args, requiresApproval, skippable };
6509
+ }
6510
+ function scoreTaskForRouting(criteria) {
6511
+ const simplicity = (criteria.taskType === "simple" ? 1 : 0) * 0.3;
6512
+ const confidence = criteria.confidence * 0.2;
6513
+ const lowRisk = !criteria.isSensitive && criteria.blastRadius < 3 ? 0.2 : 0;
6514
+ const knownCodebase = criteria.codebaseFreshness === "fresh" ? 0.15 : 0;
6515
+ const cheapComplexity = criteria.complexity === "cheap" ? 0.15 : 0;
6516
+ const total = simplicity + confidence + lowRisk + knownCodebase + cheapComplexity;
6421
6517
  return {
6422
- runId,
6423
- totalTokens: u.inputTokens + u.outputTokens + u.reasoningTokens + u.cacheReadTokens + u.cacheWriteTokens,
6424
- inputTokens: u.inputTokens,
6425
- outputTokens: u.outputTokens,
6426
- reasoningTokens: u.reasoningTokens,
6427
- cacheReadTokens: u.cacheReadTokens,
6428
- cacheWriteTokens: u.cacheWriteTokens,
6429
- messageCount: u.messageCount
6518
+ simplicity,
6519
+ confidence,
6520
+ lowRisk,
6521
+ knownCodebase,
6522
+ cheapComplexity,
6523
+ total
6430
6524
  };
6431
6525
  }
6432
- function estimateTokens(text, kind = "prose") {
6433
- if (!text || text.length === 0)
6434
- return 0;
6435
- const coefficient = ESTIMATION_COEFFICIENTS[kind] ?? ESTIMATION_COEFFICIENTS.prose;
6436
- return Math.max(0, Math.round(text.length / coefficient));
6437
- }
6438
-
6439
- // src/services/context-ingress.ts
6440
- var DEFAULT_OPTIONS = {
6441
- maxEvents: 20,
6442
- eventMaxAgeMinutes: 30,
6526
+ function buildAdaptiveStageSequence(criteria) {
6527
+ const scores = scoreTaskForRouting(criteria);
6528
+ const totalScore = scores.total;
6529
+ let workflowClass;
6530
+ let stages;
6531
+ let reason;
6532
+ if (totalScore >= 0.75 && (criteria.taskType === "simple" || criteria.taskType === "docs")) {
6533
+ workflowClass = "quick";
6534
+ stages = [
6535
+ stage("execute", "fd-execute", false, true),
6536
+ stage("verify", "fd-verify", false, true)
6537
+ ];
6538
+ reason = `Quick workflow: score ${totalScore.toFixed(2)} >= 0.75 for ${criteria.taskType} task`;
6539
+ } else if (criteria.taskType === "bugfix") {
6540
+ workflowClass = "bugfix";
6541
+ stages = [
6542
+ stage("discuss", "fd-discuss", false, false),
6543
+ stage("fix-bug", "fd-fix-bug", false, false),
6544
+ stage("verify", "fd-verify", false, false)
6545
+ ];
6546
+ reason = "Bugfix workflow: task type is bugfix";
6547
+ } else if (criteria.taskType === "docs" && totalScore < 0.75) {
6548
+ workflowClass = "docs-only";
6549
+ stages = [
6550
+ stage("write-docs", "fd-write-docs", false, false),
6551
+ stage("verify", "fd-verify", false, true)
6552
+ ];
6553
+ reason = `Docs-only workflow: score ${totalScore.toFixed(2)} < 0.75 for docs task`;
6554
+ } else if (criteria.taskType === "ui-feature") {
6555
+ workflowClass = "ui-heavy";
6556
+ stages = [
6557
+ stage("discuss", "fd-discuss", false, false),
6558
+ stage("design", "fd-design", false, false, "--mode=draft"),
6559
+ stage("plan", "fd-plan", true, false),
6560
+ stage("execute", "fd-execute", false, false),
6561
+ stage("verify", "fd-verify", false, false)
6562
+ ];
6563
+ reason = "UI-heavy workflow: task type indicates UI-heavy work";
6564
+ } else if (criteria.blastRadius >= 5 || criteria.isSensitive) {
6565
+ workflowClass = "verify-heavy";
6566
+ stages = [
6567
+ stage("plan", "fd-plan", true, false),
6568
+ stage("execute", "fd-execute", false, false),
6569
+ stage("verify", "fd-verify", false, false)
6570
+ ];
6571
+ reason = `Verify-heavy workflow: blastRadius=${criteria.blastRadius}, isSensitive=${criteria.isSensitive}`;
6572
+ } else if (criteria.confidence < 0.6 || criteria.taskType === "ambiguous") {
6573
+ workflowClass = "explore";
6574
+ stages = [
6575
+ stage("discuss", "fd-discuss", false, false),
6576
+ stage("plan", "fd-plan", true, false),
6577
+ stage("execute", "fd-execute", false, false),
6578
+ stage("verify", "fd-verify", false, false)
6579
+ ];
6580
+ reason = `Explore workflow: confidence=${criteria.confidence}, taskType=${criteria.taskType}`;
6581
+ } else {
6582
+ workflowClass = "standard";
6583
+ stages = [
6584
+ stage("plan", "fd-plan", true, false),
6585
+ stage("execute", "fd-execute", false, false),
6586
+ stage("verify", "fd-verify", false, false)
6587
+ ];
6588
+ reason = `Standard workflow: score ${totalScore.toFixed(2)} with taskType ${criteria.taskType}`;
6589
+ }
6590
+ return {
6591
+ workflowClass,
6592
+ stages,
6593
+ criteria,
6594
+ scores,
6595
+ reason
6596
+ };
6597
+ }
6598
+
6599
+ // src/services/quick-router.ts
6600
+ var BUG_SIGNALS = [
6601
+ "fix",
6602
+ "bug",
6603
+ "broken",
6604
+ "not working",
6605
+ "doesn't work",
6606
+ "does not work",
6607
+ "error",
6608
+ "crash",
6609
+ "regression",
6610
+ "debug",
6611
+ "exception",
6612
+ "failing",
6613
+ "fails",
6614
+ "incorrect",
6615
+ "wrong output",
6616
+ "infinite loop",
6617
+ "null pointer",
6618
+ "undefined",
6619
+ "404",
6620
+ "500",
6621
+ "stack trace",
6622
+ "traceback",
6623
+ "root cause",
6624
+ "why is"
6625
+ ];
6626
+ var UI_SIGNALS = [
6627
+ "landing page",
6628
+ "dashboard",
6629
+ "admin panel",
6630
+ "admin page",
6631
+ "app screen",
6632
+ "onboarding",
6633
+ "onboard",
6634
+ "wireframe",
6635
+ "mockup",
6636
+ "design system",
6637
+ "component library",
6638
+ "ui component",
6639
+ "ux flow",
6640
+ "user interface",
6641
+ "web app",
6642
+ "web application",
6643
+ "website",
6644
+ "frontend page",
6645
+ "mobile screen",
6646
+ "login page",
6647
+ "signup page",
6648
+ "settings page",
6649
+ "profile page",
6650
+ "modal",
6651
+ "dialog",
6652
+ "sidebar",
6653
+ "navigation",
6654
+ "navbar",
6655
+ "header",
6656
+ "footer",
6657
+ "layout",
6658
+ "responsive",
6659
+ "accessibility",
6660
+ "a11y",
6661
+ "dark mode",
6662
+ "theme"
6663
+ ];
6664
+ var DOCS_SIGNALS = [
6665
+ "docs",
6666
+ "documentation",
6667
+ "readme",
6668
+ "api docs",
6669
+ "usage guide",
6670
+ "write docs",
6671
+ "document",
6672
+ "document the",
6673
+ "how to use",
6674
+ "tutorial",
6675
+ "changelog",
6676
+ "contributing guide",
6677
+ "docstring",
6678
+ "jsdoc",
6679
+ "tsdoc"
6680
+ ];
6681
+ var SIMPLE_SIGNALS = [
6682
+ "rename",
6683
+ "move file",
6684
+ "quick",
6685
+ "minor",
6686
+ "small change",
6687
+ "one-liner",
6688
+ "typo",
6689
+ "update constant",
6690
+ "update config",
6691
+ "bump version"
6692
+ ];
6693
+ var AMBIGUOUS_PATTERNS = [
6694
+ /^(improve|make|update|change|add|remove|help|do|run|check|use)\s+\w+$/i
6695
+ ];
6696
+ function classifyTask(description) {
6697
+ const lower = description.toLowerCase().trim();
6698
+ if (!lower) {
6699
+ return _ambiguous([], "What task do you want to run? Please describe what you need done.");
6700
+ }
6701
+ const bugHits = BUG_SIGNALS.filter((s) => lower.includes(s));
6702
+ const uiHits = UI_SIGNALS.filter((s) => lower.includes(s));
6703
+ const docsHits = DOCS_SIGNALS.filter((s) => lower.includes(s));
6704
+ const simpleHits = SIMPLE_SIGNALS.filter((s) => lower.includes(s));
6705
+ const bugScore = Math.min(bugHits.length * 0.35, 1);
6706
+ const uiScore = Math.min(uiHits.length * 0.3, 1);
6707
+ const docsScore = Math.min(docsHits.length * 0.4, 1);
6708
+ const simpleScore = Math.min(simpleHits.length * 0.45, 1);
6709
+ if (bugScore >= 0.35 && bugScore >= uiScore && bugScore >= docsScore) {
6710
+ return {
6711
+ taskType: "bugfix",
6712
+ confidence: Math.min(0.5 + bugScore * 0.5, 0.98),
6713
+ signals: bugHits,
6714
+ requiresDesign: false,
6715
+ requiresTDD: true,
6716
+ stageSequence: buildStageSequence("bugfix"),
6717
+ clarificationNeeded: bugScore < 0.5,
6718
+ clarificationPrompt: bugScore < 0.5 ? "Can you describe the specific bug? What is the expected vs actual behavior?" : undefined
6719
+ };
6720
+ }
6721
+ if (uiScore >= 0.3) {
6722
+ return {
6723
+ taskType: "ui-feature",
6724
+ confidence: Math.min(0.5 + uiScore * 0.45, 0.95),
6725
+ signals: uiHits,
6726
+ requiresDesign: true,
6727
+ requiresTDD: true,
6728
+ stageSequence: buildStageSequence("ui-feature"),
6729
+ clarificationNeeded: false
6730
+ };
6731
+ }
6732
+ if (docsScore >= 0.4 && docsScore >= bugScore) {
6733
+ return {
6734
+ taskType: "docs",
6735
+ confidence: Math.min(0.55 + docsScore * 0.4, 0.95),
6736
+ signals: docsHits,
6737
+ requiresDesign: false,
6738
+ requiresTDD: false,
6739
+ stageSequence: buildStageSequence("docs"),
6740
+ clarificationNeeded: false
6741
+ };
6742
+ }
6743
+ if (simpleScore >= 0.45) {
6744
+ return {
6745
+ taskType: "simple",
6746
+ confidence: Math.min(0.55 + simpleScore * 0.35, 0.9),
6747
+ signals: simpleHits,
6748
+ requiresDesign: false,
6749
+ requiresTDD: false,
6750
+ stageSequence: buildStageSequence("simple"),
6751
+ clarificationNeeded: false
6752
+ };
6753
+ }
6754
+ const wordCount = lower.split(/\s+/).filter(Boolean).length;
6755
+ if (wordCount >= 5) {
6756
+ return {
6757
+ taskType: "feature",
6758
+ confidence: Math.min(0.5 + wordCount * 0.02, 0.85),
6759
+ signals: [],
6760
+ requiresDesign: false,
6761
+ requiresTDD: true,
6762
+ stageSequence: buildStageSequence("feature"),
6763
+ clarificationNeeded: wordCount < 8,
6764
+ clarificationPrompt: wordCount < 8 ? "Is this a new feature, a bug fix, or a documentation task? A bit more context will help route it correctly." : undefined
6765
+ };
6766
+ }
6767
+ const isAmbiguousPattern = AMBIGUOUS_PATTERNS.some((p) => p.test(lower));
6768
+ if (isAmbiguousPattern || wordCount < 5) {
6769
+ return _ambiguous([], "Can you describe the task in more detail? For example: is it a new feature, a bug fix, a UI change, or documentation?");
6770
+ }
6771
+ return {
6772
+ taskType: "feature",
6773
+ confidence: 0.6,
6774
+ signals: [],
6775
+ requiresDesign: false,
6776
+ requiresTDD: true,
6777
+ stageSequence: buildStageSequence("feature"),
6778
+ clarificationNeeded: false
6779
+ };
6780
+ }
6781
+ function _ambiguous(signals, prompt) {
6782
+ return {
6783
+ taskType: "ambiguous",
6784
+ confidence: 0,
6785
+ signals,
6786
+ requiresDesign: false,
6787
+ requiresTDD: false,
6788
+ stageSequence: [],
6789
+ clarificationNeeded: true,
6790
+ clarificationPrompt: prompt
6791
+ };
6792
+ }
6793
+ function buildStageSequence(taskType) {
6794
+ switch (taskType) {
6795
+ case "feature":
6796
+ return [
6797
+ stage2("discuss", "fd-discuss", false, false),
6798
+ stage2("plan", "fd-plan", true, false),
6799
+ stage2("execute", "fd-execute", false, false),
6800
+ stage2("verify", "fd-verify", false, false)
6801
+ ];
6802
+ case "ui-feature":
6803
+ return [
6804
+ stage2("discuss", "fd-discuss", false, false),
6805
+ stage2("design", "fd-design", false, false, "--mode=draft"),
6806
+ stage2("plan", "fd-plan", true, false),
6807
+ stage2("execute", "fd-execute", false, false),
6808
+ stage2("verify", "fd-verify", false, false)
6809
+ ];
6810
+ case "bugfix":
6811
+ return [
6812
+ stage2("discuss", "fd-discuss", false, false),
6813
+ stage2("fix-bug", "fd-fix-bug", false, false),
6814
+ stage2("verify", "fd-verify", false, false)
6815
+ ];
6816
+ case "docs":
6817
+ return [
6818
+ stage2("discuss", "fd-discuss", false, false),
6819
+ stage2("write-docs", "fd-write-docs", false, false),
6820
+ stage2("verify", "fd-verify", false, true)
6821
+ ];
6822
+ case "simple":
6823
+ return [
6824
+ stage2("execute", "fd-execute", false, false),
6825
+ stage2("verify", "fd-verify", false, true)
6826
+ ];
6827
+ case "ambiguous":
6828
+ return [];
6829
+ default:
6830
+ return [];
6831
+ }
6832
+ }
6833
+ function buildAdaptiveWorkflow(description, exploration) {
6834
+ const base = exploration ? classifyTaskWithContext(description, exploration) : classifyTask(description);
6835
+ const complexityResult = classifyTaskComplexity(description);
6836
+ const criteria = {
6837
+ taskType: base.taskType,
6838
+ complexity: complexityResult.complexity,
6839
+ confidence: base.confidence,
6840
+ blastRadius: 0,
6841
+ isSensitive: false,
6842
+ codebaseFreshness: exploration ? "fresh" : "unknown",
6843
+ requiresTests: base.requiresTDD
6844
+ };
6845
+ const route = buildAdaptiveStageSequence(criteria);
6846
+ return {
6847
+ ...base,
6848
+ stageSequence: route.stages,
6849
+ workflowClass: route.workflowClass,
6850
+ scores: route.scores
6851
+ };
6852
+ }
6853
+ function stage2(name, command, requiresApproval, skippable, args) {
6854
+ return { name, command, args, requiresApproval, skippable };
6855
+ }
6856
+ function classifyTaskWithContext(description, exploration, sessionHistory = []) {
6857
+ const base = classifyTask(description);
6858
+ if (!base.clarificationNeeded) {
6859
+ return base;
6860
+ }
6861
+ const refinement = refineClassification(base.clarificationPrompt ?? "", exploration, sessionHistory);
6862
+ if (!refinement.clarificationStillNeeded) {
6863
+ const resolvedType = base.taskType === "ambiguous" ? "feature" : base.taskType;
6864
+ return {
6865
+ ...base,
6866
+ taskType: resolvedType,
6867
+ stageSequence: buildStageSequence(resolvedType),
6868
+ clarificationNeeded: false,
6869
+ clarificationPrompt: undefined,
6870
+ confidence: Math.max(base.confidence, 0.55)
6871
+ };
6872
+ }
6873
+ const enrichedPrompt = refinement.supervisorContext ? `${base.clarificationPrompt ?? ""} (Context: ${refinement.supervisorContext})` : base.clarificationPrompt;
6874
+ return {
6875
+ ...base,
6876
+ clarificationPrompt: enrichedPrompt
6877
+ };
6878
+ }
6879
+
6880
+ // src/services/prompt-cache.ts
6881
+ import { createHash as createHash3 } from "crypto";
6882
+ var DEFAULT_PROMPT_CACHE_TTL_MS = 5 * 60 * 1000;
6883
+
6884
+ class PromptCacheImpl {
6885
+ store = new Map;
6886
+ getKey(description, stage3, languages) {
6887
+ const normalized = description.trim().toLowerCase().replace(/\s+/g, " ");
6888
+ const sortedLanguages = [...languages].sort().join(",");
6889
+ const payload = `${normalized}|${stage3}|${sortedLanguages}`;
6890
+ return createHash3("sha256").update(payload).digest("hex");
6891
+ }
6892
+ get(key) {
6893
+ const entry = this.store.get(key);
6894
+ if (!entry)
6895
+ return;
6896
+ if (Date.now() >= entry.expiresAt) {
6897
+ this.store.delete(key);
6898
+ return;
6899
+ }
6900
+ return entry.fragment;
6901
+ }
6902
+ set(key, fragment, ttlMs = DEFAULT_PROMPT_CACHE_TTL_MS) {
6903
+ this.store.set(key, {
6904
+ fragment,
6905
+ expiresAt: Date.now() + Math.max(0, ttlMs)
6906
+ });
6907
+ }
6908
+ invalidate() {
6909
+ this.store.clear();
6910
+ invalidateCache();
6911
+ }
6912
+ }
6913
+ var sharedPromptCache = new PromptCacheImpl;
6914
+
6915
+ // src/services/token-metrics.ts
6916
+ var ESTIMATION_COEFFICIENTS = {
6917
+ prose: 4,
6918
+ code: 3.5,
6919
+ json: 3.5
6920
+ };
6921
+ var usageByRun = new Map;
6922
+ function toPositiveInt(value) {
6923
+ if (typeof value === "string") {
6924
+ const n = Number(value);
6925
+ return Number.isFinite(n) && n >= 0 ? Math.floor(n) : 0;
6926
+ }
6927
+ if (typeof value === "number" && Number.isFinite(value) && value >= 0) {
6928
+ return Math.floor(value);
6929
+ }
6930
+ return 0;
6931
+ }
6932
+ function getAccumulated(runId) {
6933
+ return usageByRun.get(runId) ?? {
6934
+ inputTokens: 0,
6935
+ outputTokens: 0,
6936
+ reasoningTokens: 0,
6937
+ cacheReadTokens: 0,
6938
+ cacheWriteTokens: 0,
6939
+ messageCount: 0
6940
+ };
6941
+ }
6942
+ function recordMessageUsage(runId, usage) {
6943
+ const current = getAccumulated(runId);
6944
+ const next = {
6945
+ inputTokens: current.inputTokens + toPositiveInt(usage.input),
6946
+ outputTokens: current.outputTokens + toPositiveInt(usage.output),
6947
+ reasoningTokens: current.reasoningTokens + toPositiveInt(usage.reasoning),
6948
+ cacheReadTokens: current.cacheReadTokens + toPositiveInt(usage.cache?.read),
6949
+ cacheWriteTokens: current.cacheWriteTokens + toPositiveInt(usage.cache?.write),
6950
+ messageCount: current.messageCount + 1
6951
+ };
6952
+ usageByRun.set(runId, next);
6953
+ }
6954
+ function getRunUsage(runId) {
6955
+ const u = getAccumulated(runId);
6956
+ return {
6957
+ runId,
6958
+ totalTokens: u.inputTokens + u.outputTokens + u.reasoningTokens + u.cacheReadTokens + u.cacheWriteTokens,
6959
+ inputTokens: u.inputTokens,
6960
+ outputTokens: u.outputTokens,
6961
+ reasoningTokens: u.reasoningTokens,
6962
+ cacheReadTokens: u.cacheReadTokens,
6963
+ cacheWriteTokens: u.cacheWriteTokens,
6964
+ messageCount: u.messageCount
6965
+ };
6966
+ }
6967
+ function estimateTokens(text, kind = "prose") {
6968
+ if (!text || text.length === 0)
6969
+ return 0;
6970
+ const coefficient = ESTIMATION_COEFFICIENTS[kind] ?? ESTIMATION_COEFFICIENTS.prose;
6971
+ return Math.max(0, Math.round(text.length / coefficient));
6972
+ }
6973
+
6974
+ // src/services/context-ingress.ts
6975
+ var DEFAULT_OPTIONS = {
6976
+ maxEvents: 20,
6977
+ eventMaxAgeMinutes: 30,
6443
6978
  planTruncateThreshold: 8000,
6444
6979
  planTruncateTo: 4000,
6445
6980
  totalTokenBudget: Number(process.env.FLOWDECK_CONTEXT_LIMIT) || 200000
@@ -6664,9 +7199,10 @@ class ContextIngressService {
6664
7199
  const codebaseDocs = trivial.isTrivialChat ? {} : this.readCodebaseDocs(input.projectRoot);
6665
7200
  const recentEvents = trivial.isTrivialChat ? [] : this.readRecentEvents(input.projectRoot);
6666
7201
  const route = this.buildRoute(input.description);
7202
+ const workflowDecision = input.workflowDecision ?? buildAdaptiveWorkflow(input.description);
6667
7203
  const relevantRules = trivial.isTrivialChat ? [] : this.selectRelevantRules(input.projectRoot, input.description, state, route, input.currentStage);
6668
7204
  const relevantSkills = trivial.isTrivialChat ? [] : this.selectRelevantSkills(input.description);
6669
- const used = estimateTokens(JSON.stringify(state), "json") + estimateTokens(planContent, "prose") + estimateTokens(JSON.stringify(codebaseDocs), "prose") + estimateTokens(JSON.stringify(recentEvents), "json") + estimateTokens(JSON.stringify(relevantRules), "json") + estimateTokens(JSON.stringify(relevantSkills), "json");
7205
+ const used = estimateTokens(JSON.stringify(state), "json") + estimateTokens(planContent, "prose") + estimateTokens(JSON.stringify(codebaseDocs), "prose") + estimateTokens(JSON.stringify(recentEvents), "json") + estimateTokens(JSON.stringify(relevantRules), "json") + estimateTokens(JSON.stringify(relevantSkills), "json") + estimateTokens(JSON.stringify(workflowDecision), "json");
6670
7206
  const total = this.options.totalTokenBudget;
6671
7207
  const actualUsage = getRunUsage(input.sessionId);
6672
7208
  const hasActualTokens = actualUsage.totalTokens > 0;
@@ -6689,7 +7225,8 @@ class ContextIngressService {
6689
7225
  recentEvents,
6690
7226
  selectedRules: relevantRules,
6691
7227
  selectedSkills: relevantSkills,
6692
- tokenBudget
7228
+ tokenBudget,
7229
+ workflowDecision
6693
7230
  };
6694
7231
  this._assembledBySession.set(input.sessionId, ctx);
6695
7232
  this.persistBudgetSnapshot(ctx);
@@ -6716,12 +7253,12 @@ class ContextIngressService {
6716
7253
  }
6717
7254
  }
6718
7255
  buildPromptFragment(ctx) {
6719
- const stage = String(ctx.planningState.phase ?? "discuss");
7256
+ const stage3 = String(ctx.planningState.phase ?? "discuss");
6720
7257
  if (ctx.isTrivialChat) {
6721
- return `Greeting. Phase: ${stage}.`;
7258
+ return `Greeting. Phase: ${stage3}.`;
6722
7259
  }
6723
7260
  const languages = getCachedLanguages(ctx.directory);
6724
- const cacheKey = sharedPromptCache.getKey(ctx.description, stage, languages);
7261
+ const cacheKey = sharedPromptCache.getKey(ctx.description, stage3, languages);
6725
7262
  const cached = sharedPromptCache.get(cacheKey);
6726
7263
  if (cached)
6727
7264
  return cached;
@@ -6729,7 +7266,7 @@ class ContextIngressService {
6729
7266
  "# FlowDeck Context",
6730
7267
  "",
6731
7268
  `Task: ${ctx.description}`,
6732
- `Phase: ${stage}`,
7269
+ `Phase: ${stage3}`,
6733
7270
  "",
6734
7271
  "## Selected rules"
6735
7272
  ];
@@ -6756,6 +7293,22 @@ class ContextIngressService {
6756
7293
  lines.push(`blockers: ${ctx.planningState.blockers.join(", ")}`);
6757
7294
  }
6758
7295
  lines.push(`next_action: ${ctx.planningState.next_action}`);
7296
+ lines.push("", "## Workflow Decision");
7297
+ const decision = ctx.workflowDecision;
7298
+ if (decision) {
7299
+ lines.push(`workflowClass: ${decision.workflowClass ?? decision.taskType}`);
7300
+ lines.push(`taskType: ${decision.taskType}`);
7301
+ lines.push(`confidence: ${decision.confidence.toFixed(2)}`);
7302
+ lines.push(`requiresDesign: ${decision.requiresDesign}`);
7303
+ lines.push(`requiresTDD: ${decision.requiresTDD}`);
7304
+ lines.push(`stageSequence: ${decision.stageSequence.map((s) => s.name).join(" → ")}`);
7305
+ if (decision.clarificationNeeded) {
7306
+ lines.push(`clarificationNeeded: true`);
7307
+ lines.push(`clarificationPrompt: ${decision.clarificationPrompt ?? "(none provided)"}`);
7308
+ }
7309
+ } else {
7310
+ lines.push("_No workflow decision available._");
7311
+ }
6759
7312
  lines.push("", "## Recent events");
6760
7313
  if (ctx.recentEvents.length === 0) {
6761
7314
  lines.push("_No recent events._");
@@ -6776,7 +7329,8 @@ class ContextIngressService {
6776
7329
  const eventsTokens = estimateTokens(JSON.stringify(ctx.recentEvents), "json");
6777
7330
  const rulesTokens = estimateTokens(JSON.stringify(ctx.selectedRules), "json");
6778
7331
  const skillsTokens = estimateTokens(JSON.stringify(ctx.selectedSkills), "json");
6779
- const totalFallbackTokens = stateTokens + planTokens + docsTokens + eventsTokens + rulesTokens + skillsTokens;
7332
+ const workflowTokens = estimateTokens(JSON.stringify(ctx.workflowDecision), "json");
7333
+ const totalFallbackTokens = stateTokens + planTokens + docsTokens + eventsTokens + rulesTokens + skillsTokens + workflowTokens;
6780
7334
  const actualUsage = getRunUsage(ctx.sessionID);
6781
7335
  const hasActualTokens = actualUsage.totalTokens > 0;
6782
7336
  const totalUsed = hasActualTokens ? actualUsage.totalTokens : ctx.tokenBudget.usedTokens;
@@ -6805,7 +7359,8 @@ class ContextIngressService {
6805
7359
  docs: makeSnapshot("docs", docsTokens),
6806
7360
  events: makeSnapshot("events", eventsTokens),
6807
7361
  rules: makeSnapshot("rules", rulesTokens),
6808
- skills: makeSnapshot("skills", skillsTokens)
7362
+ skills: makeSnapshot("skills", skillsTokens),
7363
+ workflow: makeSnapshot("workflow", workflowTokens)
6809
7364
  };
6810
7365
  }
6811
7366
  getTotalBudget(sessionID) {
@@ -6918,12 +7473,12 @@ class ContextIngressService {
6918
7473
  const sharedDir = join19(__dir, "..", "agents", "shared");
6919
7474
  if (!existsSync19(rulesDir))
6920
7475
  return [];
6921
- const stage = currentStage ?? this.inferStageFromRoute(route);
7476
+ const stage3 = currentStage ?? this.inferStageFromRoute(route);
6922
7477
  const languages = getCachedLanguages(projectRoot);
6923
- const cacheKey = `${projectRoot}\x00${stage ?? ""}\x00${[...languages].sort().join(",")}`;
7478
+ const cacheKey = `${projectRoot}\x00${stage3 ?? ""}\x00${[...languages].sort().join(",")}`;
6924
7479
  let selection = ruleSelectionCache.get(cacheKey);
6925
7480
  if (!selection) {
6926
- selection = selectRulePaths(rulesDir, { languages, stage });
7481
+ selection = selectRulePaths(rulesDir, { languages, stage: stage3 });
6927
7482
  ruleSelectionCache.set(cacheKey, selection);
6928
7483
  }
6929
7484
  const seen = new Set;
@@ -7289,8 +7844,8 @@ class LoopDetector {
7289
7844
  var CONTRACTS = [
7290
7845
  {
7291
7846
  agent: "orchestrator",
7292
- role: "Coordinate multi-agent execution, inspect context directly, and route specialist work when appropriate.",
7293
- allowedTaskTypes: ["orchestration", "coordination", "delegation", "phase-management"],
7847
+ role: "Coordinate multi-agent execution, inspect context directly, and route specialist work via native @agent-name mentions.",
7848
+ allowedTaskTypes: ["orchestration", "coordination", "routing", "phase-management"],
7294
7849
  requiredInputs: ["STATE.md", "PLAN.md"],
7295
7850
  expectedOutputFields: ["completed_steps", "current_phase"],
7296
7851
  allowedTools: [
@@ -7585,55 +8140,302 @@ var CONTRACTS = [
7585
8140
  escalationConditions: ["documentation scope unclear"],
7586
8141
  stopConditions: ["docs written", "user confirms completeness"],
7587
8142
  successCriteria: [
7588
- "documentation written and accurate",
7589
- "no application code changed"
8143
+ "documentation written and accurate",
8144
+ "no application code changed"
8145
+ ]
8146
+ },
8147
+ {
8148
+ agent: "doc-updater",
8149
+ role: "Update existing documentation after code changes.",
8150
+ allowedTaskTypes: ["documentation-update", "doc-sync"],
8151
+ requiredInputs: ["changed files", "change summary"],
8152
+ expectedOutputFields: ["updated_docs"],
8153
+ allowedTools: ["read", "write", "edit", "glob", "grep"],
8154
+ forbiddenActions: [
8155
+ "modify application code",
8156
+ "delete documentation without replacement"
8157
+ ],
8158
+ escalationConditions: ["documentation conflicts with implementation"],
8159
+ stopConditions: ["docs updated and synced"],
8160
+ successCriteria: ["docs reflect current code", "no application code changed"]
8161
+ },
8162
+ {
8163
+ agent: "mapper",
8164
+ role: "Map codebase to structured documentation files in .codebase/.",
8165
+ allowedTaskTypes: ["codebase-mapping", "documentation", "indexing"],
8166
+ requiredInputs: ["project root"],
8167
+ expectedOutputFields: ["codebase_docs"],
8168
+ allowedTools: ["read", "write", "edit", "bash", "glob", "grep"],
8169
+ forbiddenActions: [
8170
+ "modify application source code",
8171
+ "delete existing docs without replacement"
8172
+ ],
8173
+ escalationConditions: ["mapping conflicts with existing .codebase/ files"],
8174
+ stopConditions: ["codebase docs written"],
8175
+ successCriteria: [".codebase/ docs reflect current structure", "no application code changed"]
8176
+ },
8177
+ {
8178
+ agent: "supervisor",
8179
+ role: "Governance review layer. Inspects existing commands/agents, validates policy, returns structured approve/revise/block/escalate decision. Never creates new commands or workflows.",
8180
+ allowedTaskTypes: ["governance-review", "policy-check", "pre-execution-review", "post-stage-review"],
8181
+ requiredInputs: ["target name (command or agent)", "task context"],
8182
+ expectedOutputFields: ["decision", "targetType", "targetName", "exists", "reasons", "missingRequirements", "riskFlags", "requiredChanges", "approvalStatus", "confidenceScore"],
8183
+ allowedTools: ["read", "glob", "grep", "planning-state", "policy-engine"],
8184
+ forbiddenActions: [
8185
+ "create new commands",
8186
+ "create new workflows",
8187
+ "invent new agent names",
8188
+ "modify command intent",
8189
+ "replace orchestrator",
8190
+ "become second dispatcher",
8191
+ "execute implementation tasks",
8192
+ "write or edit source files",
8193
+ "run bash commands",
8194
+ "modify PLAN.md or STATE.md"
8195
+ ],
8196
+ escalationConditions: [
8197
+ "human approval required and not granted",
8198
+ "confidence below threshold",
8199
+ "critical policy violation with no safe path forward"
8200
+ ],
8201
+ stopConditions: ["structured decision issued", "review complete"],
8202
+ successCriteria: [
8203
+ "structured SupervisorDecision returned",
8204
+ "no new commands or workflows created",
8205
+ "existing registry not modified",
8206
+ "decision is one of: approve, revise, block, escalate"
8207
+ ]
8208
+ },
8209
+ {
8210
+ agent: "default-executor",
8211
+ role: "Execute simple, direct tasks routed by the orchestrator using full tool access.",
8212
+ allowedTaskTypes: ["execution", "quick-fix", "simple-edit", "inspect-only", "quick-answer"],
8213
+ requiredInputs: ["task description", "execution mode"],
8214
+ expectedOutputFields: ["files_touched", "summary", "verification"],
8215
+ allowedTools: ["read", "write", "edit", "bash", "glob", "grep"],
8216
+ forbiddenActions: [
8217
+ "orchestrate other agents",
8218
+ "delegate to other agents",
8219
+ "expand scope silently",
8220
+ "invent new workflows"
8221
+ ],
8222
+ escalationConditions: [
8223
+ "task touches more than 5 files",
8224
+ "architectural decision needed",
8225
+ "security-sensitive path encountered"
8226
+ ],
8227
+ stopConditions: ["task complete", "escalation required"],
8228
+ successCriteria: [
8229
+ "task completed as routed",
8230
+ "scope not expanded",
8231
+ "verification performed"
8232
+ ]
8233
+ },
8234
+ {
8235
+ agent: "code-explorer",
8236
+ role: "Explore and map unfamiliar codebases before changes. Read-only analysis.",
8237
+ allowedTaskTypes: ["exploration", "code-mapping", "call-path-tracing"],
8238
+ requiredInputs: ["area or feature to explore"],
8239
+ expectedOutputFields: ["structure", "entry_points", "key_components", "call_paths"],
8240
+ allowedTools: [
8241
+ "read",
8242
+ "glob",
8243
+ "grep",
8244
+ "codegraph",
8245
+ "codegraph-search",
8246
+ "codegraph-node",
8247
+ "codegraph-explore"
8248
+ ],
8249
+ forbiddenActions: [
8250
+ "modify files",
8251
+ "implement changes",
8252
+ "run destructive commands"
8253
+ ],
8254
+ escalationConditions: ["codebase index missing and exploration blocked"],
8255
+ stopConditions: ["exploration report complete"],
8256
+ successCriteria: [
8257
+ "report delivered",
8258
+ "no files modified"
8259
+ ]
8260
+ },
8261
+ {
8262
+ agent: "debug-specialist",
8263
+ role: "Diagnose bugs through systematic root cause analysis. Report only, do not implement fixes directly.",
8264
+ allowedTaskTypes: ["debugging", "root-cause-analysis", "bug-investigation"],
8265
+ requiredInputs: ["bug report or failure output"],
8266
+ expectedOutputFields: ["root_cause", "evidence", "call_path", "recommended_fix"],
8267
+ allowedTools: ["read", "glob", "grep", "bash"],
8268
+ forbiddenActions: [
8269
+ "implement fixes directly",
8270
+ "suppress errors without fixing",
8271
+ "modify application code"
8272
+ ],
8273
+ escalationConditions: [
8274
+ "architectural problem found",
8275
+ "security issue discovered"
8276
+ ],
8277
+ stopConditions: ["debug report complete"],
8278
+ successCriteria: [
8279
+ "root cause identified",
8280
+ "fix recommended",
8281
+ "no implementation done"
8282
+ ]
8283
+ },
8284
+ {
8285
+ agent: "build-error-resolver",
8286
+ role: "Fix build errors, compilation failures, and dependency issues with minimal changes.",
8287
+ allowedTaskTypes: ["build-fix", "type-error-fix", "dependency-fix"],
8288
+ requiredInputs: ["build error output"],
8289
+ expectedOutputFields: ["files_modified", "summary", "verification"],
8290
+ allowedTools: ["read", "write", "edit", "bash", "glob", "grep"],
8291
+ forbiddenActions: [
8292
+ "use as any or @ts-ignore to suppress type errors",
8293
+ "refactor unrelated code",
8294
+ "add features while fixing builds"
8295
+ ],
8296
+ escalationConditions: [
8297
+ "architectural problem causing build failure",
8298
+ "security issue discovered"
8299
+ ],
8300
+ stopConditions: ["build passes", "escalation required"],
8301
+ successCriteria: [
8302
+ "build and type check pass",
8303
+ "minimum changes applied",
8304
+ "no type-safety suppression without explanation"
8305
+ ]
8306
+ },
8307
+ {
8308
+ agent: "task-splitter",
8309
+ role: "Decompose complex tasks into parallel workstreams with explicit dependencies.",
8310
+ allowedTaskTypes: ["task-decomposition", "parallel-planning"],
8311
+ requiredInputs: ["complex task description"],
8312
+ expectedOutputFields: ["waves", "dependencies", "agent_assignments"],
8313
+ allowedTools: ["read", "glob", "grep", "planning-state"],
8314
+ forbiddenActions: [
8315
+ "execute tasks",
8316
+ "modify files",
8317
+ "assign non-existent agents"
8318
+ ],
8319
+ escalationConditions: ["task too small to benefit from parallelization"],
8320
+ stopConditions: ["parallel execution plan complete"],
8321
+ successCriteria: [
8322
+ "plan delivered",
8323
+ "dependencies clear",
8324
+ "no file modifications"
8325
+ ]
8326
+ },
8327
+ {
8328
+ agent: "discusser",
8329
+ role: "Extract requirements via structured Q&A and record decisions with D-XX numbering.",
8330
+ allowedTaskTypes: ["requirements-gathering", "discussion", "decision-recording"],
8331
+ requiredInputs: ["task description"],
8332
+ expectedOutputFields: ["decisions", "suppressed_questions", "open_questions"],
8333
+ allowedTools: ["read", "write", "edit", "glob", "grep", "planning-state"],
8334
+ forbiddenActions: [
8335
+ "implement features",
8336
+ "skip decision recording",
8337
+ "ask more than one question per turn"
8338
+ ],
8339
+ escalationConditions: ["conflict with prior decisions"],
8340
+ stopConditions: ["requirements complete", "plan ready"],
8341
+ successCriteria: [
8342
+ "decisions recorded",
8343
+ "no open questions remain"
8344
+ ]
8345
+ },
8346
+ {
8347
+ agent: "risk-analyst",
8348
+ role: "Assess risk of proposed changes using patch trust and regression signals. Read-only.",
8349
+ allowedTaskTypes: ["risk-assessment", "change-impact-analysis"],
8350
+ requiredInputs: ["change description", "trust signals"],
8351
+ expectedOutputFields: ["risk_level", "approval_required", "safer_alternative"],
8352
+ allowedTools: ["read", "glob", "grep"],
8353
+ forbiddenActions: [
8354
+ "modify files",
8355
+ "approve changes directly",
8356
+ "invent risk signals not in input data"
8357
+ ],
8358
+ escalationConditions: ["critical risk identified"],
8359
+ stopConditions: ["risk report complete"],
8360
+ successCriteria: [
8361
+ "structured report delivered",
8362
+ "no file modifications"
8363
+ ]
8364
+ },
8365
+ {
8366
+ agent: "policy-enforcer",
8367
+ role: "Apply policy gate matrix to decide whether a proposed edit can proceed. Read-only decision.",
8368
+ allowedTaskTypes: ["policy-enforcement", "gate-decision"],
8369
+ requiredInputs: ["file_path", "change_description", "policy_violations", "risk_score"],
8370
+ expectedOutputFields: ["decision", "trigger", "recommended_action"],
8371
+ allowedTools: ["read", "glob", "grep", "policy-engine"],
8372
+ forbiddenActions: [
8373
+ "modify files",
8374
+ "deviate from gate matrix",
8375
+ "approve blocked changes"
8376
+ ],
8377
+ escalationConditions: ["arch constraint violation"],
8378
+ stopConditions: ["gate decision issued"],
8379
+ successCriteria: [
8380
+ "decision applied per matrix",
8381
+ "no file modifications"
7590
8382
  ]
7591
8383
  },
7592
8384
  {
7593
- agent: "doc-updater",
7594
- role: "Update existing documentation after code changes.",
7595
- allowedTaskTypes: ["documentation-update", "doc-sync"],
7596
- requiredInputs: ["changed files", "change summary"],
7597
- expectedOutputFields: ["updated_docs"],
7598
- allowedTools: ["read", "write", "edit", "glob", "grep"],
8385
+ agent: "performance-optimizer",
8386
+ role: "Identify and fix performance bottlenecks using profiling data and measurements.",
8387
+ allowedTaskTypes: ["performance-optimization", "profiling", "bottleneck-analysis"],
8388
+ requiredInputs: ["performance complaint or metric"],
8389
+ expectedOutputFields: ["baseline", "bottleneck", "fix", "after_measurement"],
8390
+ allowedTools: ["read", "write", "edit", "bash", "glob", "grep"],
7599
8391
  forbiddenActions: [
7600
- "modify application code",
7601
- "delete documentation without replacement"
8392
+ "optimize without measurements",
8393
+ "refactor unrelated code",
8394
+ "skip before/after verification"
7602
8395
  ],
7603
- escalationConditions: ["documentation conflicts with implementation"],
7604
- stopConditions: ["docs updated and synced"],
7605
- successCriteria: ["docs reflect current code", "no application code changed"]
8396
+ escalationConditions: ["architectural bottleneck requiring redesign"],
8397
+ stopConditions: ["performance report complete"],
8398
+ successCriteria: [
8399
+ "before and after measurements provided",
8400
+ "improvement verified"
8401
+ ]
7606
8402
  },
7607
8403
  {
7608
- agent: "supervisor",
7609
- role: "Governance review layer. Inspects existing commands/agents, validates policy, returns structured approve/revise/block/escalate decision. Never creates new commands or workflows.",
7610
- allowedTaskTypes: ["governance-review", "policy-check", "pre-execution-review", "post-stage-review"],
7611
- requiredInputs: ["target name (command or agent)", "task context"],
7612
- expectedOutputFields: ["decision", "targetType", "targetName", "exists", "reasons", "missingRequirements", "riskFlags", "requiredChanges", "approvalStatus", "confidenceScore"],
7613
- allowedTools: ["read", "glob", "grep", "planning-state", "policy-engine"],
8404
+ agent: "refactor-guide",
8405
+ role: "Guide safe refactoring without changing behavior.",
8406
+ allowedTaskTypes: ["refactoring", "code-cleanup", "structure-improvement"],
8407
+ requiredInputs: ["refactoring target"],
8408
+ expectedOutputFields: ["transformations", "test_results"],
8409
+ allowedTools: ["read", "write", "edit", "bash", "glob", "grep"],
7614
8410
  forbiddenActions: [
7615
- "create new commands",
7616
- "create new workflows",
7617
- "invent new agent names",
7618
- "modify command intent",
7619
- "replace orchestrator",
7620
- "become second dispatcher",
7621
- "execute implementation tasks",
7622
- "write or edit source files",
7623
- "run bash commands",
7624
- "modify PLAN.md or STATE.md"
8411
+ "add features",
8412
+ "leave tests failing",
8413
+ "combine multiple transformations in one step"
7625
8414
  ],
7626
- escalationConditions: [
7627
- "human approval required and not granted",
7628
- "confidence below threshold",
7629
- "critical policy violation with no safe path forward"
8415
+ escalationConditions: ["tests fail during refactor"],
8416
+ stopConditions: ["refactor complete", "tests green"],
8417
+ successCriteria: [
8418
+ "behavior preserved",
8419
+ "tests pass"
8420
+ ]
8421
+ },
8422
+ {
8423
+ agent: "auto-learner",
8424
+ role: "Capture reusable knowledge from session artifacts as skills.",
8425
+ allowedTaskTypes: ["knowledge-capture", "skill-creation"],
8426
+ requiredInputs: ["session reflection artifacts"],
8427
+ expectedOutputFields: ["skills_created", "summary"],
8428
+ allowedTools: ["read", "reflect", "write"],
8429
+ forbiddenActions: [
8430
+ "ask user questions",
8431
+ "create routine skills",
8432
+ "create more than 3 skills per session"
7630
8433
  ],
7631
- stopConditions: ["structured decision issued", "review complete"],
8434
+ escalationConditions: ["no valuable patterns found"],
8435
+ stopConditions: ["skills written or no new skills identified"],
7632
8436
  successCriteria: [
7633
- "structured SupervisorDecision returned",
7634
- "no new commands or workflows created",
7635
- "existing registry not modified",
7636
- "decision is one of: approve, revise, block, escalate"
8437
+ "valuable patterns captured",
8438
+ "maximum 3 skills per session"
7637
8439
  ]
7638
8440
  }
7639
8441
  ];
@@ -7641,6 +8443,9 @@ var REGISTRY = new Map(CONTRACTS.map((c) => [c.agent, c]));
7641
8443
  function getContract(agent) {
7642
8444
  return REGISTRY.get(agent) ?? null;
7643
8445
  }
8446
+ function getAllContracts() {
8447
+ return [...CONTRACTS];
8448
+ }
7644
8449
 
7645
8450
  // src/services/agent-validator.ts
7646
8451
  function resolveValidatorMode(directory) {
@@ -7742,2040 +8547,1528 @@ function validateAgentOutput(agent, output) {
7742
8547
  detail: `Agent "${agent}" output missing expected field "${field}"`,
7743
8548
  severity: "warn"
7744
8549
  });
7745
- }
7746
- }
7747
- }
7748
- const hasBlock = violations.some((v) => v.severity === "block");
7749
- const hasWarn = violations.some((v) => v.severity === "warn");
7750
- let action;
7751
- if (!hasBlock && !hasWarn) {
7752
- action = "allow";
7753
- } else if (hasBlock) {
7754
- action = "block";
7755
- } else {
7756
- action = "warn";
7757
- }
7758
- return {
7759
- agent,
7760
- valid: violations.length === 0,
7761
- action,
7762
- violations,
7763
- message: violations.length > 0 ? violations.map((v) => `[${v.rule}] ${v.detail}`).join("; ") : undefined
7764
- };
7765
- }
7766
- function validateToolAccess(directory, agent, toolName, opts = {}) {
7767
- return validateAgent(directory, { agent, toolUsed: toolName, ...opts });
7768
- }
7769
-
7770
- // src/services/supervisor-binding.ts
7771
- var REGISTERED_COMMANDS = [
7772
- "fd-ask",
7773
- "fd-checkpoint",
7774
- "fd-deploy-check",
7775
- "fd-design",
7776
- "fd-discuss",
7777
- "fd-doctor",
7778
- "fd-execute",
7779
- "fd-fix-bug",
7780
- "fd-guarded-edit",
7781
- "fd-map-codebase",
7782
- "fd-multi-repo",
7783
- "fd-new-feature",
7784
- "fd-plan",
7785
- "fd-quick",
7786
- "fd-reflect",
7787
- "fd-resume",
7788
- "fd-status",
7789
- "fd-suggest",
7790
- "fd-translate-intent",
7791
- "fd-verify",
7792
- "fd-write-docs",
7793
- "fd-done"
7794
- ];
7795
- function resolveSupervisorConfig(directory) {
7796
- try {
7797
- const config = loadFlowDeckConfig(directory);
7798
- const sup = config.governance?.supervisor ?? {};
7799
- return {
7800
- enabled: sup.enabled ?? true,
7801
- mode: sup.mode ?? "advisory",
7802
- reviewedTargets: sup.reviewedTargets ?? [],
7803
- canBlock: sup.canBlock ?? true,
7804
- confidenceThreshold: sup.confidenceThreshold ?? 0.7,
7805
- postExecutionReview: sup.postExecutionReview ?? false
7806
- };
7807
- } catch {
7808
- return {
7809
- enabled: true,
7810
- mode: "advisory",
7811
- reviewedTargets: [],
7812
- canBlock: true,
7813
- confidenceThreshold: 0.7,
7814
- postExecutionReview: false
7815
- };
7816
- }
7817
- }
7818
- function isRegisteredCommand(name) {
7819
- return REGISTERED_COMMANDS.includes(name);
7820
- }
7821
- function isRegisteredAgent(name) {
7822
- return AGENT_NAMES.includes(name);
7823
- }
7824
- function isRegisteredTarget(name) {
7825
- if (isRegisteredCommand(name))
7826
- return { exists: true, type: "command" };
7827
- if (isRegisteredAgent(name))
7828
- return { exists: true, type: "agent" };
7829
- return { exists: false, type: "agent" };
7830
- }
7831
- function checkCommandPolicy(commandName, ctx) {
7832
- const reasons = [];
7833
- const riskFlags = [];
7834
- const missingRequirements = [];
7835
- const requiredChanges = [];
7836
- if (commandName === "fd-new-feature" || commandName === "fd-execute") {
7837
- const workflowClass = ctx.workflowClass;
7838
- if (workflowClass !== "quick" && workflowClass !== "docs-only") {
7839
- const taskLower = (ctx.taskDescription ?? "").toLowerCase();
7840
- const isUiHeavy = /landing page|dashboard|admin panel|website|web app|ui|ux|interface|frontend|component/.test(taskLower);
7841
- if (isUiHeavy && ctx.currentPhase === "execute" && ctx.designApprovalPresent === false) {
7842
- missingRequirements.push("design approval (design stage must complete before execute for UI-heavy tasks)");
7843
- riskFlags.push("UI-heavy task entering execute phase without design approval");
7844
- requiredChanges.push("Run /fd-design first and obtain design approval before proceeding to execute");
7845
- }
7846
- }
7847
- }
7848
- if (commandName === "fd-fix-bug") {
7849
- if (ctx.regressionTestPresent === false) {
7850
- missingRequirements.push("regression test (required before bugfix implementation)");
7851
- riskFlags.push("Bugfix command invoked without a regression test");
7852
- requiredChanges.push("Write a failing regression test before implementing the fix");
7853
- }
7854
- }
7855
- if (commandName === "fd-deploy-check") {
7856
- if (ctx.prerequisitesMet === false && ctx.missingInputs && ctx.missingInputs.length > 0) {
7857
- missingRequirements.push(...ctx.missingInputs);
7858
- riskFlags.push("Deploy check attempted with unmet prerequisites");
7859
- }
7860
- }
7861
- if (commandName === "fd-execute" && ctx.currentPhase && ctx.currentPhase !== "execute") {
7862
- const workflowClass = ctx.workflowClass;
7863
- const isQuick = workflowClass === "quick" || workflowClass === "docs-only";
7864
- if (!isQuick) {
7865
- riskFlags.push(`fd-execute invoked in phase "${ctx.currentPhase}" instead of "execute"`);
7866
- requiredChanges.push(`Ensure project phase is "execute" before running fd-execute (currently: ${ctx.currentPhase})`);
7867
- }
7868
- }
7869
- if (ctx.approvalRequired && !ctx.approvalGranted) {
7870
- missingRequirements.push("human approval (required for this command)");
7871
- riskFlags.push("Approval gate not satisfied");
7872
- requiredChanges.push("Obtain explicit human approval before proceeding");
7873
- }
7874
- const passed = missingRequirements.length === 0 && riskFlags.length === 0 && requiredChanges.length === 0;
7875
- if (passed) {
7876
- reasons.push(`Command "${commandName}" passed all policy checks`);
7877
- }
7878
- return { passed, reasons, riskFlags, missingRequirements, requiredChanges };
7879
- }
7880
- function checkAgentPolicy(agentName, ctx) {
7881
- const reasons = [];
7882
- const riskFlags = [];
7883
- const missingRequirements = [];
7884
- const requiredChanges = [];
7885
- const contract = getContract(agentName);
7886
- if (!contract) {
7887
- riskFlags.push(`Agent "${agentName}" has no registered capability contract`);
7888
- return { passed: false, reasons, riskFlags, missingRequirements, requiredChanges };
7889
- }
7890
- if (ctx.missingInputs && ctx.missingInputs.length > 0) {
7891
- for (const missing of ctx.missingInputs) {
7892
- const isRequired = contract.requiredInputs.some((r) => r.toLowerCase().includes(missing.toLowerCase()) || missing.toLowerCase().includes(r.toLowerCase()));
7893
- if (isRequired) {
7894
- missingRequirements.push(missing);
7895
- requiredChanges.push(`Provide "${missing}" before delegating to ${agentName}`);
7896
- }
7897
- }
7898
- }
7899
- if (ctx.approvalRequired && !ctx.approvalGranted) {
7900
- const needsApproval = contract.escalationConditions.some((c) => c.toLowerCase().includes("approval") || c.toLowerCase().includes("approve"));
7901
- if (needsApproval) {
7902
- missingRequirements.push("human approval");
7903
- riskFlags.push(`Agent "${agentName}" requires approval via escalation condition`);
7904
- requiredChanges.push("Obtain explicit human approval before proceeding");
7905
- }
7906
- }
7907
- if (agentName === "design" || agentName === "frontend-coder") {
7908
- const taskLower = (ctx.taskDescription ?? "").toLowerCase();
7909
- const isUiHeavy = /landing page|dashboard|admin panel|website|web app|ui|ux|interface|frontend|component/.test(taskLower);
7910
- if (agentName === "frontend-coder" && isUiHeavy && ctx.designApprovalPresent === false) {
7911
- missingRequirements.push("design handoff approval");
7912
- riskFlags.push("frontend-coder invoked for UI-heavy task without approved design handoff");
7913
- requiredChanges.push("Complete design stage and obtain design approval before delegating to frontend-coder");
7914
- }
7915
- }
7916
- const passed = missingRequirements.length === 0 && riskFlags.length === 0;
7917
- if (passed) {
7918
- reasons.push(`Agent "${agentName}" passed all policy checks`);
7919
- }
7920
- return { passed, reasons, riskFlags, missingRequirements, requiredChanges };
7921
- }
7922
- function computeConfidence(exists, policyResult, ctx) {
7923
- if (!exists)
7924
- return 0;
7925
- if (policyResult.riskFlags.length >= 3)
7926
- return 0.2;
7927
- if (policyResult.riskFlags.length === 2)
7928
- return 0.4;
7929
- if (policyResult.riskFlags.length === 1)
7930
- return 0.6;
7931
- if (policyResult.missingRequirements.length > 0)
7932
- return 0.5;
7933
- if (ctx.prerequisitesMet === false)
7934
- return 0.45;
7935
- return 0.95;
7936
- }
7937
- function resolveDecision(exists, policyResult, confidenceScore, threshold, ctx, clarificationQuestion) {
7938
- if (!exists) {
7939
- return { decision: "block", approvalStatus: "denied" };
7940
- }
7941
- if (ctx.approvalRequired && !ctx.approvalGranted) {
7942
- return { decision: "escalate", approvalStatus: "escalated", clarificationQuestion };
7943
- }
7944
- if (!policyResult.passed) {
7945
- if (policyResult.requiredChanges.length > 0) {
7946
- return { decision: "revise", approvalStatus: "pending" };
8550
+ }
7947
8551
  }
7948
- return { decision: "block", approvalStatus: "denied" };
7949
8552
  }
7950
- if (confidenceScore < threshold) {
7951
- return { decision: "escalate", approvalStatus: "escalated", clarificationQuestion };
8553
+ const hasBlock = violations.some((v) => v.severity === "block");
8554
+ const hasWarn = violations.some((v) => v.severity === "warn");
8555
+ let action;
8556
+ if (!hasBlock && !hasWarn) {
8557
+ action = "allow";
8558
+ } else if (hasBlock) {
8559
+ action = "block";
8560
+ } else {
8561
+ action = "warn";
7952
8562
  }
7953
- return { decision: "approve", approvalStatus: "approved" };
8563
+ return {
8564
+ agent,
8565
+ valid: violations.length === 0,
8566
+ action,
8567
+ violations,
8568
+ message: violations.length > 0 ? violations.map((v) => `[${v.rule}] ${v.detail}`).join("; ") : undefined
8569
+ };
7954
8570
  }
7955
- function runSupervisorReview(directory, targetName, ctx = {}, clarificationQuestion) {
7956
- const config = resolveSupervisorConfig(directory);
7957
- const reviewPhase = ctx.reviewPhase ?? "preflight";
7958
- const timestamp3 = new Date().toISOString();
7959
- if (config.reviewedTargets.length > 0 && !config.reviewedTargets.includes(targetName)) {
8571
+ function validateToolAccess(directory, agent, toolName, opts = {}) {
8572
+ return validateAgent(directory, { agent, toolUsed: toolName, ...opts });
8573
+ }
8574
+
8575
+ // src/services/supervisor-binding.ts
8576
+ var REGISTERED_COMMANDS = [
8577
+ "fd-ask",
8578
+ "fd-checkpoint",
8579
+ "fd-deploy-check",
8580
+ "fd-design",
8581
+ "fd-discuss",
8582
+ "fd-doctor",
8583
+ "fd-execute",
8584
+ "fd-fix-bug",
8585
+ "fd-guarded-edit",
8586
+ "fd-map-codebase",
8587
+ "fd-multi-repo",
8588
+ "fd-new-feature",
8589
+ "fd-plan",
8590
+ "fd-quick",
8591
+ "fd-reflect",
8592
+ "fd-resume",
8593
+ "fd-status",
8594
+ "fd-suggest",
8595
+ "fd-translate-intent",
8596
+ "fd-verify",
8597
+ "fd-write-docs",
8598
+ "fd-done"
8599
+ ];
8600
+ function resolveSupervisorConfig(directory) {
8601
+ try {
8602
+ const config = loadFlowDeckConfig(directory);
8603
+ const sup = config.governance?.supervisor ?? {};
7960
8604
  return {
7961
- decision: "approve",
7962
- targetType: "agent",
7963
- targetName,
7964
- exists: true,
7965
- reasons: [`Target "${targetName}" is not in the reviewed targets list — auto-approved`],
7966
- missingRequirements: [],
7967
- riskFlags: [],
7968
- requiredChanges: [],
7969
- approvalStatus: "approved",
7970
- confidenceScore: 1,
7971
- reviewPhase,
7972
- timestamp: timestamp3
8605
+ enabled: sup.enabled ?? true,
8606
+ mode: sup.mode ?? "advisory",
8607
+ reviewedTargets: sup.reviewedTargets ?? [],
8608
+ canBlock: sup.canBlock ?? true,
8609
+ confidenceThreshold: sup.confidenceThreshold ?? 0.7,
8610
+ postExecutionReview: sup.postExecutionReview ?? false
7973
8611
  };
7974
- }
7975
- const { exists, type: targetType } = isRegisteredTarget(targetName);
7976
- if (!exists) {
7977
- const decision2 = {
7978
- decision: "block",
7979
- targetType,
7980
- targetName,
7981
- exists: false,
7982
- reasons: [
7983
- `Target "${targetName}" is not registered in the FlowDeck command or agent registry.`,
7984
- "The supervisor does not create new commands or workflows.",
7985
- "Only registered targets can be executed."
7986
- ],
7987
- missingRequirements: [],
7988
- riskFlags: [`Unregistered target: "${targetName}"`],
7989
- requiredChanges: [
7990
- `Use one of the registered commands: ${REGISTERED_COMMANDS.join(", ")}`,
7991
- `Or use one of the registered agents: ${AGENT_NAMES.join(", ")}`
7992
- ],
7993
- approvalStatus: "denied",
7994
- confidenceScore: 0,
7995
- reviewPhase,
7996
- timestamp: timestamp3
8612
+ } catch {
8613
+ return {
8614
+ enabled: true,
8615
+ mode: "advisory",
8616
+ reviewedTargets: [],
8617
+ canBlock: true,
8618
+ confidenceThreshold: 0.7,
8619
+ postExecutionReview: false
7997
8620
  };
7998
- return decision2;
7999
8621
  }
8000
- const policyResult = targetType === "command" ? checkCommandPolicy(targetName, ctx) : checkAgentPolicy(targetName, ctx);
8001
- const confidenceScore = computeConfidence(exists, policyResult, ctx);
8002
- const { decision, approvalStatus, clarificationQuestion: escalationQuestion } = resolveDecision(exists, policyResult, confidenceScore, config.confidenceThreshold, ctx, clarificationQuestion);
8003
- const reasons = policyResult.reasons.length > 0 ? policyResult.reasons : decision === "approve" ? [`Target "${targetName}" reviewed and approved for execution`] : [`Target "${targetName}" reviewed — decision: ${decision}`];
8004
- const supervisorDecision = {
8005
- decision,
8006
- targetType,
8007
- targetName,
8008
- exists,
8009
- reasons,
8010
- missingRequirements: policyResult.missingRequirements,
8011
- riskFlags: policyResult.riskFlags,
8012
- requiredChanges: policyResult.requiredChanges,
8013
- approvalStatus,
8014
- confidenceScore,
8015
- reviewPhase,
8016
- timestamp: timestamp3,
8017
- ...escalationQuestion ? { clarificationQuestion: escalationQuestion } : {}
8018
- };
8019
- return supervisorDecision;
8020
8622
  }
8021
- function reviewToolCall(directory, input) {
8022
- const targetName = input.agent;
8023
- const ctx = {
8024
- taskDescription: input.assembledContext?.description,
8025
- currentPhase: input.currentStage,
8026
- workflowClass: input.workflowClass,
8027
- designApprovalPresent: input.assembledContext?.planningState?.design_approved ?? false,
8028
- run_id: input.runId,
8029
- session_id: input.sessionID,
8030
- reviewPhase: "preflight"
8031
- };
8032
- const review = runSupervisorReview(directory, targetName, ctx);
8033
- const riskFlags = review.riskFlags;
8034
- const baseReason = review.reasons.join("; ");
8035
- switch (review.decision) {
8036
- case "approve":
8037
- return {
8038
- verdict: "allow",
8039
- reason: baseReason,
8040
- riskFlags,
8041
- source: "supervisor"
8042
- };
8043
- case "block":
8044
- return {
8045
- verdict: "deny",
8046
- reason: baseReason,
8047
- riskFlags,
8048
- source: "supervisor",
8049
- escalationMessage: review.requiredChanges.length > 0 ? review.requiredChanges.join("; ") : `Supervisor blocked ${targetName}`
8050
- };
8051
- case "revise":
8052
- return {
8053
- verdict: "ask",
8054
- reason: baseReason,
8055
- riskFlags,
8056
- source: "supervisor"
8057
- };
8058
- case "escalate":
8059
- return {
8060
- verdict: "ask",
8061
- reason: baseReason + (review.clarificationQuestion ? ` ${review.clarificationQuestion}` : ""),
8062
- riskFlags,
8063
- source: "supervisor"
8064
- };
8065
- }
8623
+ function isRegisteredCommand(name) {
8624
+ return REGISTERED_COMMANDS.includes(name);
8066
8625
  }
8067
-
8068
- // src/hooks/tool-guard.ts
8069
- import { existsSync as existsSync20, readFileSync as readFileSync20 } from "fs";
8070
- import { join as join20 } from "path";
8071
-
8072
- // src/lib/task-routing.ts
8073
- var UI_HEAVY_KEYWORDS = [
8074
- "landing page",
8075
- "marketing site",
8076
- "website",
8077
- "web app",
8078
- "mobile app",
8079
- "app screen",
8080
- "dashboard",
8081
- "admin panel",
8082
- "settings page",
8083
- "onboarding ux",
8084
- "kanban",
8085
- "design system",
8086
- "responsive",
8087
- "ui",
8088
- "ux",
8089
- "cta",
8090
- "conversion flow",
8091
- "saas interface",
8092
- "user-facing"
8093
- ];
8094
- var NON_UI_KEYWORDS = [
8095
- "backend",
8096
- "infrastructure",
8097
- "migration",
8098
- "pipeline",
8099
- "api only",
8100
- "database only",
8101
- "cli",
8102
- "worker"
8103
- ];
8104
- function isUiHeavyTask(input) {
8105
- const normalized = input.trim().toLowerCase();
8106
- if (!normalized)
8107
- return false;
8108
- const hasUiSignal = UI_HEAVY_KEYWORDS.some((keyword) => normalized.includes(keyword));
8109
- if (!hasUiSignal)
8110
- return false;
8111
- const hasOnlyNonUiSignals = NON_UI_KEYWORDS.some((keyword) => normalized.includes(keyword)) && !normalized.includes("frontend");
8112
- return !hasOnlyNonUiSignals;
8626
+ function isRegisteredAgent(name) {
8627
+ return AGENT_NAMES.includes(name);
8113
8628
  }
8114
-
8115
- // src/hooks/tool-guard.ts
8116
- var BLOCKED_PATTERNS = {
8117
- read: [".env", ".pem", ".key", ".secret"],
8118
- write: ["node_modules"],
8119
- bash: [
8120
- "rm -rf",
8121
- "rm -fr",
8122
- "rm --recursive --force",
8123
- "rm -r -f",
8124
- "mkfs",
8125
- "mkfs.ext4",
8126
- "mkfs.ext3",
8127
- "mkfs.xfs",
8128
- "mkfs.btrfs",
8129
- "dd if=",
8130
- "dd of=",
8131
- "chmod 777 /",
8132
- "chmod -r 777 /",
8133
- "mv / /dev/null",
8134
- ">/dev/sda",
8135
- ":(){ :|:& };:"
8136
- ]
8137
- };
8138
- function extractTargetPath(args) {
8139
- return String(args?.path ?? args?.file_path ?? args?.filename ?? args?.filePath ?? "");
8629
+ function isRegisteredTarget(name) {
8630
+ if (isRegisteredCommand(name))
8631
+ return { exists: true, type: "command" };
8632
+ if (isRegisteredAgent(name))
8633
+ return { exists: true, type: "agent" };
8634
+ return { exists: false, type: "agent" };
8140
8635
  }
8141
- function normalizeBashCommand(cmd) {
8142
- let normalized = cmd.toLowerCase().replace(/#.*$/gm, "").replace(/\s+/g, " ").replace(/\s*;\s*/g, "; ").replace(/\s*\|\s*/g, " | ").replace(/--recursive/g, "-r").replace(/--force/g, "-f").trim();
8143
- let prev = normalized;
8144
- while (true) {
8145
- const next = prev.replace(/(\s-[a-z])\s+-([a-z])/g, "$1$2");
8146
- if (next === prev)
8147
- break;
8148
- prev = next;
8636
+ function checkCommandPolicy(commandName, ctx) {
8637
+ const reasons = [];
8638
+ const riskFlags = [];
8639
+ const missingRequirements = [];
8640
+ const requiredChanges = [];
8641
+ if (commandName === "fd-new-feature" || commandName === "fd-execute") {
8642
+ const workflowClass = ctx.workflowClass;
8643
+ if (workflowClass !== "quick" && workflowClass !== "docs-only") {
8644
+ const taskLower = (ctx.taskDescription ?? "").toLowerCase();
8645
+ const isUiHeavy = /landing page|dashboard|admin panel|website|web app|ui|ux|interface|frontend|component/.test(taskLower);
8646
+ if (isUiHeavy && ctx.currentPhase === "execute" && ctx.designApprovalPresent === false) {
8647
+ missingRequirements.push("design approval (design stage must complete before execute for UI-heavy tasks)");
8648
+ riskFlags.push("UI-heavy task entering execute phase without design approval");
8649
+ requiredChanges.push("Run /fd-design first and obtain design approval before proceeding to execute");
8650
+ }
8651
+ }
8149
8652
  }
8150
- return prev;
8151
- }
8152
- function isPipedToShell(normalized) {
8153
- const segments = normalized.split(" | ").map((s) => s.trim());
8154
- const downloaders = new Set(["curl", "wget"]);
8155
- const shells = new Set(["sh", "bash", "zsh", "fish"]);
8156
- for (let i = 0;i < segments.length - 1; i++) {
8157
- const first = segments[i].split(/\s+/)[0];
8158
- const next = segments[i + 1].split(/\s+/)[0];
8159
- if (downloaders.has(first) && shells.has(next))
8160
- return true;
8653
+ if (commandName === "fd-fix-bug") {
8654
+ if (ctx.regressionTestPresent === false) {
8655
+ missingRequirements.push("regression test (required before bugfix implementation)");
8656
+ riskFlags.push("Bugfix command invoked without a regression test");
8657
+ requiredChanges.push("Write a failing regression test before implementing the fix");
8658
+ }
8161
8659
  }
8162
- return false;
8163
- }
8164
- function isBashCommandDangerous(cmd) {
8165
- const normalized = normalizeBashCommand(cmd);
8166
- for (const p of BLOCKED_PATTERNS.bash) {
8167
- if (normalized.includes(p)) {
8168
- return `FLOWDECK: Dangerous bash command blocked (matched "${p}").`;
8660
+ if (commandName === "fd-deploy-check") {
8661
+ if (ctx.prerequisitesMet === false && ctx.missingInputs && ctx.missingInputs.length > 0) {
8662
+ missingRequirements.push(...ctx.missingInputs);
8663
+ riskFlags.push("Deploy check attempted with unmet prerequisites");
8169
8664
  }
8170
8665
  }
8171
- if (isPipedToShell(normalized)) {
8172
- return "FLOWDECK: Piping curl/wget directly to a shell is blocked.";
8666
+ if (commandName === "fd-execute" && ctx.currentPhase && ctx.currentPhase !== "execute") {
8667
+ const workflowClass = ctx.workflowClass;
8668
+ const isQuick = workflowClass === "quick" || workflowClass === "docs-only";
8669
+ if (!isQuick) {
8670
+ riskFlags.push(`fd-execute invoked in phase "${ctx.currentPhase}" instead of "execute"`);
8671
+ requiredChanges.push(`Ensure project phase is "execute" before running fd-execute (currently: ${ctx.currentPhase})`);
8672
+ }
8173
8673
  }
8174
- if (/\bdd\b/.test(normalized) && /\/dev\/(sd|hd|nvme|mmcblk|disk)/.test(normalized)) {
8175
- return "FLOWDECK: dd command targeting a block device is blocked.";
8674
+ if (ctx.approvalRequired && !ctx.approvalGranted) {
8675
+ missingRequirements.push("human approval (required for this command)");
8676
+ riskFlags.push("Approval gate not satisfied");
8677
+ requiredChanges.push("Obtain explicit human approval before proceeding");
8176
8678
  }
8177
- if (/:\s*\(\s*\)\s*\{\s*:\s*\|\s*:\s*&\s*}\s*;\s*:/.test(normalized)) {
8178
- return "FLOWDECK: Fork bomb pattern is blocked.";
8679
+ const passed = missingRequirements.length === 0 && riskFlags.length === 0 && requiredChanges.length === 0;
8680
+ if (passed) {
8681
+ reasons.push(`Command "${commandName}" passed all policy checks`);
8179
8682
  }
8180
- return null;
8683
+ return { passed, reasons, riskFlags, missingRequirements, requiredChanges };
8181
8684
  }
8182
- function isBlocked(tool13, args) {
8183
- const patterns = BLOCKED_PATTERNS[tool13];
8184
- if (!patterns)
8185
- return null;
8186
- if (tool13 === "bash") {
8187
- const cmd = args.command;
8188
- if (!cmd)
8189
- return null;
8190
- return isBashCommandDangerous(cmd);
8685
+ function checkAgentPolicy(agentName, ctx) {
8686
+ const reasons = [];
8687
+ const riskFlags = [];
8688
+ const missingRequirements = [];
8689
+ const requiredChanges = [];
8690
+ const contract = getContract(agentName);
8691
+ if (!contract) {
8692
+ riskFlags.push(`Agent "${agentName}" has no registered capability contract`);
8693
+ return { passed: false, reasons, riskFlags, missingRequirements, requiredChanges };
8191
8694
  }
8192
- if (tool13 === "read" || tool13 === "write") {
8193
- const filePath = extractTargetPath(args);
8194
- if (!filePath)
8195
- return null;
8196
- for (const p of patterns) {
8197
- if (filePath.includes(p)) {
8198
- return tool13 === "read" ? `FLOWDECK: Access to "${p}" files is blocked.` : `FLOWDECK: Writing to "${p}" is blocked.`;
8695
+ if (ctx.missingInputs && ctx.missingInputs.length > 0) {
8696
+ for (const missing of ctx.missingInputs) {
8697
+ const isRequired = contract.requiredInputs.some((r) => r.toLowerCase().includes(missing.toLowerCase()) || missing.toLowerCase().includes(r.toLowerCase()));
8698
+ if (isRequired) {
8699
+ missingRequirements.push(missing);
8700
+ requiredChanges.push(`Provide "${missing}" before delegating to ${agentName}`);
8199
8701
  }
8200
8702
  }
8201
- return null;
8202
8703
  }
8203
- return null;
8204
- }
8205
- function checkArchConstraint(directory, filePath) {
8206
- const constraintsPath = join20(codebaseDir(directory), "CONSTRAINTS.md");
8207
- if (!existsSync20(constraintsPath))
8208
- return null;
8209
- try {
8210
- const content = readFileSync20(constraintsPath, "utf-8");
8211
- const match = content.match(/## Forbidden Paths\n([\s\S]*?)(?:\n##|$)/);
8212
- if (!match)
8213
- return null;
8214
- for (const line of match[1].split(`
8215
- `)) {
8216
- const pattern = line.replace(/^-\s*/, "").split("#")[0].trim();
8217
- if (pattern && filePath.includes(pattern)) {
8218
- return `FLOWDECK [arch-constraint]: editing "${pattern}" is forbidden by .codebase/CONSTRAINTS.md`;
8219
- }
8220
- }
8221
- } catch {}
8222
- return null;
8223
- }
8224
- function checkPhaseEnforcement(directory) {
8225
- try {
8226
- const state = readPlanningState(directory);
8227
- const flowdeckConfig = resolveDesignFirstConfig(loadFlowDeckConfig(directory));
8228
- if (state.phase > 0 && state.phase < 3) {
8229
- return `FLOWDECK [phase-gate]: writing to codebase is blocked in phase ${state.phase} (${state.phase === 1 ? "discuss" : "plan"}). Run /fd-plan --confirm to enter execute phase.`;
8704
+ if (ctx.approvalRequired && !ctx.approvalGranted) {
8705
+ const needsApproval = contract.escalationConditions.some((c) => c.toLowerCase().includes("approval") || c.toLowerCase().includes("approve"));
8706
+ if (needsApproval) {
8707
+ missingRequirements.push("human approval");
8708
+ riskFlags.push(`Agent "${agentName}" requires approval via escalation condition`);
8709
+ requiredChanges.push("Obtain explicit human approval before proceeding");
8230
8710
  }
8231
- if (flowdeckConfig.enabled && flowdeckConfig.requireApprovalBeforeImplementation && isUiDesignApprovalRequired(directory)) {
8232
- if (flowdeckConfig.enforcement === "advisory") {
8233
- return `FLOWDECK [design-gate]: advisory design-first mode detected missing approval. Run /fd-design --mode=draft or set design_override=true in STATE.md.`;
8234
- }
8235
- return `FLOWDECK [design-gate]: UI-heavy task requires approved design handoff before implementation. Run /fd-design --mode=draft and ensure design_stage=handoff_complete + design_approved=true, or set explicit design_override with reason.`;
8711
+ }
8712
+ if (agentName === "design" || agentName === "frontend-coder") {
8713
+ const taskLower = (ctx.taskDescription ?? "").toLowerCase();
8714
+ const isUiHeavy = /landing page|dashboard|admin panel|website|web app|ui|ux|interface|frontend|component/.test(taskLower);
8715
+ if (agentName === "frontend-coder" && isUiHeavy && ctx.designApprovalPresent === false) {
8716
+ missingRequirements.push("design handoff approval");
8717
+ riskFlags.push("frontend-coder invoked for UI-heavy task without approved design handoff");
8718
+ requiredChanges.push("Complete design stage and obtain design approval before delegating to frontend-coder");
8236
8719
  }
8237
- } catch {}
8238
- return null;
8239
- }
8240
- function isUiDesignApprovalRequired(directory) {
8241
- const state = readPlanningState(directory);
8242
- if (state.design_override && state.design_override_reason && state.design_override_reason.trim().length > 0)
8243
- return false;
8244
- if (state.requires_design_first) {
8245
- return !(state.design_stage === "handoff_complete" && state.design_approved);
8246
8720
  }
8247
- if (state.task_type && isUiHeavyTask(state.task_type)) {
8248
- return !(state.design_stage === "handoff_complete" && state.design_approved);
8721
+ const passed = missingRequirements.length === 0 && riskFlags.length === 0;
8722
+ if (passed) {
8723
+ reasons.push(`Agent "${agentName}" passed all policy checks`);
8249
8724
  }
8250
- const planPath = phasePlanPath(directory, state.phase || 1);
8251
- if (!existsSync20(planPath))
8252
- return false;
8253
- const planContent = readFileSync20(planPath, "utf-8");
8254
- if (!isUiHeavyTask(planContent))
8255
- return false;
8256
- return !(state.design_stage === "handoff_complete" && state.design_approved);
8725
+ return { passed, reasons, riskFlags, missingRequirements, requiredChanges };
8257
8726
  }
8258
- function allow(reason, riskFlags = []) {
8259
- return { verdict: "allow", reason, riskFlags, source: "tool-guard" };
8727
+ function computeConfidence(exists, policyResult, ctx) {
8728
+ if (!exists)
8729
+ return 0;
8730
+ if (policyResult.riskFlags.length >= 3)
8731
+ return 0.2;
8732
+ if (policyResult.riskFlags.length === 2)
8733
+ return 0.4;
8734
+ if (policyResult.riskFlags.length === 1)
8735
+ return 0.6;
8736
+ if (policyResult.missingRequirements.length > 0)
8737
+ return 0.5;
8738
+ if (ctx.prerequisitesMet === false)
8739
+ return 0.45;
8740
+ return 0.95;
8260
8741
  }
8261
- function deny(reason, escalationMessage, riskFlags = []) {
8262
- return {
8263
- verdict: "deny",
8264
- reason,
8265
- riskFlags,
8266
- source: "tool-guard",
8267
- escalationMessage
8742
+ function resolveDecision(exists, policyResult, confidenceScore, threshold, ctx, clarificationQuestion) {
8743
+ if (!exists) {
8744
+ return { decision: "block", approvalStatus: "denied" };
8745
+ }
8746
+ if (ctx.approvalRequired && !ctx.approvalGranted) {
8747
+ return { decision: "escalate", approvalStatus: "escalated", clarificationQuestion };
8748
+ }
8749
+ if (!policyResult.passed) {
8750
+ if (policyResult.requiredChanges.length > 0) {
8751
+ return { decision: "revise", approvalStatus: "pending" };
8752
+ }
8753
+ return { decision: "block", approvalStatus: "denied" };
8754
+ }
8755
+ if (confidenceScore < threshold) {
8756
+ return { decision: "escalate", approvalStatus: "escalated", clarificationQuestion };
8757
+ }
8758
+ return { decision: "approve", approvalStatus: "approved" };
8759
+ }
8760
+ function runSupervisorReview(directory, targetName, ctx = {}, clarificationQuestion) {
8761
+ const config = resolveSupervisorConfig(directory);
8762
+ const reviewPhase = ctx.reviewPhase ?? "preflight";
8763
+ const timestamp3 = new Date().toISOString();
8764
+ if (config.reviewedTargets.length > 0 && !config.reviewedTargets.includes(targetName)) {
8765
+ return {
8766
+ decision: "approve",
8767
+ targetType: "agent",
8768
+ targetName,
8769
+ exists: true,
8770
+ reasons: [`Target "${targetName}" is not in the reviewed targets list — auto-approved`],
8771
+ missingRequirements: [],
8772
+ riskFlags: [],
8773
+ requiredChanges: [],
8774
+ approvalStatus: "approved",
8775
+ confidenceScore: 1,
8776
+ reviewPhase,
8777
+ timestamp: timestamp3
8778
+ };
8779
+ }
8780
+ const { exists, type: targetType } = isRegisteredTarget(targetName);
8781
+ if (!exists) {
8782
+ const decision2 = {
8783
+ decision: "block",
8784
+ targetType,
8785
+ targetName,
8786
+ exists: false,
8787
+ reasons: [
8788
+ `Target "${targetName}" is not registered in the FlowDeck command or agent registry.`,
8789
+ "The supervisor does not create new commands or workflows.",
8790
+ "Only registered targets can be executed."
8791
+ ],
8792
+ missingRequirements: [],
8793
+ riskFlags: [`Unregistered target: "${targetName}"`],
8794
+ requiredChanges: [
8795
+ `Use one of the registered commands: ${REGISTERED_COMMANDS.join(", ")}`,
8796
+ `Or use one of the registered agents: ${AGENT_NAMES.join(", ")}`
8797
+ ],
8798
+ approvalStatus: "denied",
8799
+ confidenceScore: 0,
8800
+ reviewPhase,
8801
+ timestamp: timestamp3
8802
+ };
8803
+ return decision2;
8804
+ }
8805
+ const policyResult = targetType === "command" ? checkCommandPolicy(targetName, ctx) : checkAgentPolicy(targetName, ctx);
8806
+ const confidenceScore = computeConfidence(exists, policyResult, ctx);
8807
+ const { decision, approvalStatus, clarificationQuestion: escalationQuestion } = resolveDecision(exists, policyResult, confidenceScore, config.confidenceThreshold, ctx, clarificationQuestion);
8808
+ const reasons = policyResult.reasons.length > 0 ? policyResult.reasons : decision === "approve" ? [`Target "${targetName}" reviewed and approved for execution`] : [`Target "${targetName}" reviewed — decision: ${decision}`];
8809
+ const supervisorDecision = {
8810
+ decision,
8811
+ targetType,
8812
+ targetName,
8813
+ exists,
8814
+ reasons,
8815
+ missingRequirements: policyResult.missingRequirements,
8816
+ riskFlags: policyResult.riskFlags,
8817
+ requiredChanges: policyResult.requiredChanges,
8818
+ approvalStatus,
8819
+ confidenceScore,
8820
+ reviewPhase,
8821
+ timestamp: timestamp3,
8822
+ ...escalationQuestion ? { clarificationQuestion: escalationQuestion } : {}
8268
8823
  };
8824
+ return supervisorDecision;
8269
8825
  }
8270
- function evaluate(input) {
8271
- const { directory, tool: tool13, args } = input;
8272
- if (tool13 !== "bash" && tool13 !== "read" && tool13 !== "write" && tool13 !== "edit") {
8273
- return allow("Tool is not guardable");
8274
- }
8275
- const blocked = isBlocked(tool13, args);
8276
- if (blocked) {
8277
- return deny(blocked, blocked, ["dangerous-pattern"]);
8278
- }
8279
- if (tool13 === "write" || tool13 === "edit") {
8280
- const phaseBlock = checkPhaseEnforcement(directory);
8281
- if (phaseBlock) {
8282
- const isAdvisory = phaseBlock.includes("[design-gate]: advisory");
8283
- if (isAdvisory) {
8284
- return allow(phaseBlock, ["design-gate-advisory"]);
8285
- }
8286
- return deny(phaseBlock, phaseBlock, ["phase-gate"]);
8287
- }
8288
- const filePath = extractTargetPath(args);
8289
- const constraintBlock = checkArchConstraint(directory, filePath);
8290
- if (constraintBlock) {
8291
- return deny(constraintBlock, constraintBlock, ["arch-constraint"]);
8292
- }
8826
+ function reviewToolCall(directory, input) {
8827
+ const targetName = input.agent;
8828
+ const ctx = {
8829
+ taskDescription: input.assembledContext?.description,
8830
+ currentPhase: input.currentStage,
8831
+ workflowClass: input.workflowClass,
8832
+ designApprovalPresent: input.assembledContext?.planningState?.design_approved ?? false,
8833
+ run_id: input.runId,
8834
+ session_id: input.sessionID,
8835
+ reviewPhase: "preflight"
8836
+ };
8837
+ const review = runSupervisorReview(directory, targetName, ctx);
8838
+ const riskFlags = review.riskFlags;
8839
+ const baseReason = review.reasons.join("; ");
8840
+ switch (review.decision) {
8841
+ case "approve":
8842
+ return {
8843
+ verdict: "allow",
8844
+ reason: baseReason,
8845
+ riskFlags,
8846
+ source: "supervisor"
8847
+ };
8848
+ case "block":
8849
+ return {
8850
+ verdict: "deny",
8851
+ reason: baseReason,
8852
+ riskFlags,
8853
+ source: "supervisor",
8854
+ escalationMessage: review.requiredChanges.length > 0 ? review.requiredChanges.join("; ") : `Supervisor blocked ${targetName}`
8855
+ };
8856
+ case "revise":
8857
+ return {
8858
+ verdict: "ask",
8859
+ reason: baseReason,
8860
+ riskFlags,
8861
+ source: "supervisor"
8862
+ };
8863
+ case "escalate":
8864
+ return {
8865
+ verdict: "ask",
8866
+ reason: baseReason + (review.clarificationQuestion ? ` ${review.clarificationQuestion}` : ""),
8867
+ riskFlags,
8868
+ source: "supervisor"
8869
+ };
8293
8870
  }
8294
- return allow("Tool guard passed");
8295
8871
  }
8296
8872
 
8297
- // src/hooks/guard-rails.ts
8298
- import { existsSync as existsSync21, readFileSync as readFileSync21 } from "fs";
8299
- import { join as join21 } from "path";
8300
- var PLANNING_DIR2 = ".planning";
8301
- var CONFIG_FILE = "config.json";
8302
- var STATE_FILE2 = "STATE.md";
8303
- function resolveExecutionMode(configPath, trustScore, volatility) {
8304
- if (existsSync21(configPath)) {
8305
- try {
8306
- const config = JSON.parse(readFileSync21(configPath, "utf-8"));
8307
- if (config.execution_mode === "review-only")
8308
- return "review-only";
8309
- if (config.execution_mode === "guarded")
8310
- return "guarded";
8311
- if (config.execution_mode === "auto")
8312
- return "auto";
8313
- } catch {}
8314
- }
8315
- if (trustScore !== null) {
8316
- if (trustScore < 30)
8317
- return "review-only";
8318
- if (trustScore < 60)
8319
- return "guarded";
8320
- }
8321
- if (volatility === "critical")
8322
- return "review-only";
8323
- if (volatility === "volatile")
8324
- return "guarded";
8325
- return "auto";
8326
- }
8327
- var BUILD_DEPLOY_PATTERNS = [
8328
- "npm build",
8329
- "npm run build",
8330
- "bun build",
8331
- "yarn build",
8332
- "npm deploy",
8333
- "yarn deploy",
8334
- "bun deploy",
8335
- "npm install",
8336
- "yarn install",
8337
- "bun install",
8338
- "make build",
8339
- "make deploy",
8340
- "docker build",
8341
- "docker push",
8342
- "docker-compose",
8343
- "git push",
8344
- "git deploy",
8345
- "gradle build",
8346
- "mvn package",
8347
- "ant build",
8348
- "cargo build",
8349
- "cargo deploy",
8350
- "python setup.py",
8351
- "pip install",
8352
- "rails deploy",
8353
- "rake deploy"
8873
+ // src/hooks/tool-guard.ts
8874
+ import { existsSync as existsSync20, readFileSync as readFileSync20 } from "fs";
8875
+ import { join as join20 } from "path";
8876
+
8877
+ // src/lib/task-routing.ts
8878
+ var UI_HEAVY_KEYWORDS = [
8879
+ "landing page",
8880
+ "marketing site",
8881
+ "website",
8882
+ "web app",
8883
+ "mobile app",
8884
+ "app screen",
8885
+ "dashboard",
8886
+ "admin panel",
8887
+ "settings page",
8888
+ "onboarding ux",
8889
+ "kanban",
8890
+ "design system",
8891
+ "responsive",
8892
+ "ui",
8893
+ "ux",
8894
+ "cta",
8895
+ "conversion flow",
8896
+ "saas interface",
8897
+ "user-facing"
8354
8898
  ];
8355
- function allow2(reason, riskFlags = []) {
8356
- return { verdict: "allow", reason, riskFlags, source: "guard-rails" };
8357
- }
8358
- function deny2(reason, escalationMessage, riskFlags = []) {
8359
- return {
8360
- verdict: "deny",
8361
- reason,
8362
- riskFlags,
8363
- source: "guard-rails",
8364
- escalationMessage
8365
- };
8899
+ var NON_UI_KEYWORDS = [
8900
+ "backend",
8901
+ "infrastructure",
8902
+ "migration",
8903
+ "pipeline",
8904
+ "api only",
8905
+ "database only",
8906
+ "cli",
8907
+ "worker"
8908
+ ];
8909
+ function isUiHeavyTask(input) {
8910
+ const normalized = input.trim().toLowerCase();
8911
+ if (!normalized)
8912
+ return false;
8913
+ const hasUiSignal = UI_HEAVY_KEYWORDS.some((keyword) => normalized.includes(keyword));
8914
+ if (!hasUiSignal)
8915
+ return false;
8916
+ const hasOnlyNonUiSignals = NON_UI_KEYWORDS.some((keyword) => normalized.includes(keyword)) && !normalized.includes("frontend");
8917
+ return !hasOnlyNonUiSignals;
8366
8918
  }
8367
- function evaluate2(input) {
8368
- const { directory, tool: tool13, args } = input;
8369
- const planningDirPath = join21(directory, PLANNING_DIR2);
8370
- const codebaseDirectory = codebaseDir(directory);
8371
- const configPath = join21(planningDirPath, CONFIG_FILE);
8372
- const statePath3 = join21(planningDirPath, STATE_FILE2);
8373
- const workspaceRoot = findWorkspaceRoot(directory);
8374
- if (workspaceRoot && directory !== workspaceRoot) {
8375
- const workspaceConfig = getWorkspaceConfig(directory);
8376
- if (workspaceConfig && workspaceConfig.workspace_mode === "shared" && !existsSync21(planningDirPath)) {
8377
- const msg = `No .planning/ in this sub-repo. Switch to workspace root: cd ${workspaceRoot}`;
8378
- return deny2(msg, `[flowdeck] BLOCK: ${msg}`, ["workspace-shared-mode"]);
8379
- }
8380
- }
8381
- if (tool13 === "write" || tool13 === "edit") {
8382
- if (!existsSync21(planningDirPath)) {
8383
- return allow2("FlowDeck not initialized in this directory — skipping guard-rails");
8384
- }
8385
- if (!existsSync21(codebaseDirectory)) {
8386
- const msg = ".codebase/ not found. Run /fd-map-codebase to map the codebase.";
8387
- return allow2(msg, ["codebase-missing"]);
8388
- }
8389
- const execMode = resolveExecutionMode(configPath, null);
8390
- if (execMode === "review-only") {
8391
- const msg = "review-only mode: propose diff but do not apply. Set execution_mode in .planning/config.json to change.";
8392
- return deny2(msg, `[flowdeck] BLOCK (${msg})`, ["review-only-mode"]);
8393
- }
8394
- if (execMode === "guarded") {
8395
- return allow2("guarded mode: edit will proceed but flag for human review", ["guarded-mode"]);
8396
- }
8397
- const designGateMessage = getDesignGateMessage(directory);
8398
- if (designGateMessage) {
8399
- if (designGateMessage.startsWith("[flowdeck] WARNING:")) {
8400
- return allow2(designGateMessage, ["design-gate-advisory"]);
8401
- }
8402
- return deny2(designGateMessage, designGateMessage, ["design-gate"]);
8403
- }
8404
- const severity = effectiveSeverity(configPath, statePath3);
8405
- if (severity === null) {
8406
- return allow2("guard_enforcement is off");
8407
- }
8408
- if (severity === "warn") {
8409
- const warning = getWarningMessage(planningDirPath);
8410
- return allow2(`[flowdeck] WARNING: ${warning}`, ["plan-not-confirmed"]);
8411
- }
8412
- const blockMessage = getBlockMessage(planningDirPath);
8413
- return deny2(blockMessage, `[flowdeck] BLOCK: ${blockMessage}`, ["plan-not-confirmed"]);
8919
+
8920
+ // src/hooks/tool-guard.ts
8921
+ var BLOCKED_PATTERNS = {
8922
+ read: [".env", ".pem", ".key", ".secret"],
8923
+ write: ["node_modules"],
8924
+ bash: [
8925
+ "rm -rf",
8926
+ "rm -fr",
8927
+ "rm --recursive --force",
8928
+ "rm -r -f",
8929
+ "mkfs",
8930
+ "mkfs.ext4",
8931
+ "mkfs.ext3",
8932
+ "mkfs.xfs",
8933
+ "mkfs.btrfs",
8934
+ "dd if=",
8935
+ "dd of=",
8936
+ "chmod 777 /",
8937
+ "chmod -r 777 /",
8938
+ "mv / /dev/null",
8939
+ ">/dev/sda",
8940
+ ":(){ :|:& };:"
8941
+ ]
8942
+ };
8943
+ function extractTargetPath(args) {
8944
+ return String(args?.path ?? args?.file_path ?? args?.filename ?? args?.filePath ?? "");
8945
+ }
8946
+ function normalizeBashCommand(cmd) {
8947
+ let normalized = cmd.toLowerCase().replace(/#.*$/gm, "").replace(/\s+/g, " ").replace(/\s*;\s*/g, "; ").replace(/\s*\|\s*/g, " | ").replace(/--recursive/g, "-r").replace(/--force/g, "-f").trim();
8948
+ let prev = normalized;
8949
+ while (true) {
8950
+ const next = prev.replace(/(\s-[a-z])\s+-([a-z])/g, "$1$2");
8951
+ if (next === prev)
8952
+ break;
8953
+ prev = next;
8414
8954
  }
8415
- if (tool13 === "bash") {
8416
- const cmd = String(args?.command || "");
8417
- for (const pattern of BUILD_DEPLOY_PATTERNS) {
8418
- if (cmd.includes(pattern)) {
8419
- if (!getPlanConfirmed(statePath3)) {
8420
- const msg = "Build/deploy command detected but plan is not confirmed. Run /fd-plan first.";
8421
- return allow2(`[flowdeck] WARNING: ${msg}`, ["build-deploy-without-plan"]);
8422
- }
8423
- break;
8424
- }
8425
- }
8955
+ return prev;
8956
+ }
8957
+ function isPipedToShell(normalized) {
8958
+ const segments = normalized.split(" | ").map((s) => s.trim());
8959
+ const downloaders = new Set(["curl", "wget"]);
8960
+ const shells = new Set(["sh", "bash", "zsh", "fish"]);
8961
+ for (let i = 0;i < segments.length - 1; i++) {
8962
+ const first = segments[i].split(/\s+/)[0];
8963
+ const next = segments[i + 1].split(/\s+/)[0];
8964
+ if (downloaders.has(first) && shells.has(next))
8965
+ return true;
8426
8966
  }
8427
- return allow2("Guard rails passed");
8967
+ return false;
8428
8968
  }
8429
- function getDesignGateMessage(dir) {
8430
- const designConfig = resolveDesignFirstConfig(loadFlowDeckConfig(dir));
8431
- if (!designConfig.enabled || !designConfig.requireApprovalBeforeImplementation)
8432
- return null;
8433
- const state = readPlanningState(dir);
8434
- if (state.design_override && state.design_override_reason && state.design_override_reason.trim().length > 0)
8435
- return null;
8436
- const designApproved = state.design_stage === "handoff_complete" && state.design_approved;
8437
- if (state.requires_design_first || state.task_type && isUiHeavyTask(state.task_type) || planSuggestsUiHeavy(dir, state.phase || 1)) {
8438
- if (designApproved)
8439
- return null;
8440
- if (designConfig.enforcement === "advisory") {
8441
- return "[flowdeck] WARNING: UI-heavy task detected without approved design handoff. Run /fd-design --mode=draft first.";
8969
+ function isBashCommandDangerous(cmd) {
8970
+ const normalized = normalizeBashCommand(cmd);
8971
+ for (const p of BLOCKED_PATTERNS.bash) {
8972
+ if (normalized.includes(p)) {
8973
+ return `FLOWDECK: Dangerous bash command blocked (matched "${p}").`;
8442
8974
  }
8443
- return "[flowdeck] BLOCK: UI-heavy task requires approved design handoff. Run /fd-design --mode=draft or set explicit design override in STATE.md.";
8444
8975
  }
8445
- return null;
8446
- }
8447
- function planSuggestsUiHeavy(dir, phase) {
8448
- const planPath = phasePlanPath(dir, phase);
8449
- if (!existsSync21(planPath))
8450
- return false;
8451
- const planContent = readFileSync21(planPath, "utf-8");
8452
- return isUiHeavyTask(planContent);
8453
- }
8454
- function effectiveSeverity(configPath, statePath3) {
8455
- if (existsSync21(configPath)) {
8456
- try {
8457
- const configContent = readFileSync21(configPath, "utf-8");
8458
- const config = JSON.parse(configContent);
8459
- if (config.guard_enforcement === "warn")
8460
- return "warn";
8461
- if (config.guard_enforcement === "block")
8462
- return "block";
8463
- if (config.guard_enforcement === "off")
8464
- return null;
8465
- } catch {}
8976
+ if (isPipedToShell(normalized)) {
8977
+ return "FLOWDECK: Piping curl/wget directly to a shell is blocked.";
8466
8978
  }
8467
- return getPlanConfirmed(statePath3) ? "block" : "warn";
8468
- }
8469
- function getPlanConfirmed(statePath3) {
8470
- if (!existsSync21(statePath3))
8471
- return false;
8472
- try {
8473
- const content = readFileSync21(statePath3, "utf-8");
8474
- const match = content.match(/plan_confirmed:\s*(true|false)/i);
8475
- return match ? match[1].toLowerCase() === "true" : false;
8476
- } catch {
8477
- return false;
8979
+ if (/\bdd\b/.test(normalized) && /\/dev\/(sd|hd|nvme|mmcblk|disk)/.test(normalized)) {
8980
+ return "FLOWDECK: dd command targeting a block device is blocked.";
8478
8981
  }
8479
- }
8480
- function getWarningMessage(planningDir2) {
8481
- if (!existsSync21(join21(planningDir2, STATE_FILE2))) {
8482
- return "No STATE.md found. Run /fd-map-codebase then /fd-new-feature to start a feature.";
8982
+ if (/:\s*\(\s*\)\s*\{\s*:\s*\|\s*:\s*&\s*}\s*;\s*:/.test(normalized)) {
8983
+ return "FLOWDECK: Fork bomb pattern is blocked.";
8483
8984
  }
8484
- return "Plan not confirmed. Run /fd-plan and confirm to enable execution.";
8985
+ return null;
8485
8986
  }
8486
- function getBlockMessage(planningDir2) {
8487
- if (!existsSync21(join21(planningDir2, STATE_FILE2))) {
8488
- return "No STATE.md found. Run /fd-map-codebase then /fd-new-feature to start a feature.";
8987
+ function isBlocked(tool13, args) {
8988
+ const patterns = BLOCKED_PATTERNS[tool13];
8989
+ if (!patterns)
8990
+ return null;
8991
+ if (tool13 === "bash") {
8992
+ const cmd = args.command;
8993
+ if (!cmd)
8994
+ return null;
8995
+ return isBashCommandDangerous(cmd);
8489
8996
  }
8490
- return "Plan not confirmed. Run /fd-plan and confirm to enable execution.";
8491
- }
8492
-
8493
- // src/services/approval-manager.ts
8494
- import { existsSync as existsSync22, readFileSync as readFileSync22, writeFileSync as writeFileSync13, mkdirSync as mkdirSync12 } from "fs";
8495
- import { join as join22 } from "path";
8496
- import { createHash as createHash4 } from "crypto";
8497
- import { randomUUID } from "crypto";
8498
- var APPROVAL_TTL_MS = 30 * 60 * 1000;
8499
- var SENSITIVE_PATTERNS = [
8500
- /auth/i,
8501
- /login/i,
8502
- /password/i,
8503
- /secret/i,
8504
- /token/i,
8505
- /jwt/i,
8506
- /session/i,
8507
- /oauth/i,
8508
- /payment/i,
8509
- /billing/i,
8510
- /stripe/i,
8511
- /credit/i,
8512
- /migration/i,
8513
- /migrate/i,
8514
- /schema/i,
8515
- /alembic/i,
8516
- /infra/i,
8517
- /terraform/i,
8518
- /ansible/i,
8519
- /k8s/i,
8520
- /kubernetes/i,
8521
- /docker/i,
8522
- /\.env/i,
8523
- /secrets\./i,
8524
- /config\/prod/i,
8525
- /production/i,
8526
- /admin/i,
8527
- /privilege/i,
8528
- /sudo/i
8529
- ];
8530
- function isSensitivePath(filePath) {
8531
- return SENSITIVE_PATTERNS.some((p) => p.test(filePath));
8532
- }
8533
- function approvalsPath(dir) {
8534
- return join22(codebaseDir(dir), "APPROVALS.json");
8535
- }
8536
- function loadStore(dir) {
8537
- const p = approvalsPath(dir);
8538
- if (!existsSync22(p))
8539
- return { requests: [] };
8540
- try {
8541
- return JSON.parse(readFileSync22(p, "utf-8"));
8542
- } catch {
8543
- return { requests: [] };
8997
+ if (tool13 === "read" || tool13 === "write") {
8998
+ const filePath = extractTargetPath(args);
8999
+ if (!filePath)
9000
+ return null;
9001
+ for (const p of patterns) {
9002
+ if (filePath.includes(p)) {
9003
+ return tool13 === "read" ? `FLOWDECK: Access to "${p}" files is blocked.` : `FLOWDECK: Writing to "${p}" is blocked.`;
9004
+ }
9005
+ }
9006
+ return null;
8544
9007
  }
9008
+ return null;
8545
9009
  }
8546
- function saveStore(dir, store) {
8547
- const cd = codebaseDir(dir);
8548
- if (!existsSync22(cd))
8549
- mkdirSync12(cd, { recursive: true });
8550
- writeFileSync13(approvalsPath(dir), JSON.stringify(store, null, 2), "utf-8");
8551
- }
8552
- function requestApproval(dir, run_id, trigger, reason, options = {}) {
8553
- const store = loadStore(dir);
8554
- const req = {
8555
- id: randomUUID(),
8556
- run_id,
8557
- session_id: options.session_id ?? "session-0",
8558
- requested_at: new Date().toISOString(),
8559
- status: "pending",
8560
- trigger,
8561
- reason,
8562
- risk_score: options.risk_score ?? 0,
8563
- ...options.agent ? { agent: options.agent } : {},
8564
- ...options.file_path ? { file_path: options.file_path } : {},
8565
- ...options.content_hash ? { content_hash: options.content_hash } : {},
8566
- ...options.change_description ? { change_description: options.change_description } : {}
8567
- };
8568
- store.requests.push(req);
8569
- saveStore(dir, store);
8570
- return req;
8571
- }
8572
- function checkApproval(dir, criteria) {
8573
- const store = loadStore(dir);
8574
- const now = Date.now();
8575
- return store.requests.filter((r) => r.status === "approved" && r.resolved_at && r.run_id === criteria.run_id && r.session_id === criteria.session_id && r.agent === criteria.agent && r.file_path === criteria.file_path && r.content_hash === criteria.content_hash && now - new Date(r.resolved_at).getTime() < APPROVAL_TTL_MS).sort((a, b) => b.resolved_at.localeCompare(a.resolved_at)).at(0) ?? null;
8576
- }
8577
- function computeContentHash(args) {
8578
- const keys = Object.keys(args).sort();
8579
- const payload = JSON.stringify(args, keys);
8580
- return createHash4("sha256").update(payload).digest("hex").slice(0, 16);
8581
- }
8582
- function requestApprovalForTool(dir, run_id, session_id, agent, tool13, args) {
8583
- const filePath = extractTargetPath2(args);
8584
- const isSensitive = filePath ? isSensitivePath(filePath) : false;
8585
- return requestApproval(dir, run_id, tool13, `Approval required for tool "${tool13}"`, {
8586
- file_path: filePath,
8587
- risk_score: isSensitive ? 30 : 50,
8588
- session_id,
8589
- agent,
8590
- content_hash: computeContentHash(args),
8591
- change_description: `Tool "${tool13}" requested on ${filePath || "unknown target"}`
8592
- });
9010
+ function checkArchConstraint(directory, filePath) {
9011
+ const constraintsPath = join20(codebaseDir(directory), "CONSTRAINTS.md");
9012
+ if (!existsSync20(constraintsPath))
9013
+ return null;
9014
+ try {
9015
+ const content = readFileSync20(constraintsPath, "utf-8");
9016
+ const match = content.match(/## Forbidden Paths\n([\s\S]*?)(?:\n##|$)/);
9017
+ if (!match)
9018
+ return null;
9019
+ for (const line of match[1].split(`
9020
+ `)) {
9021
+ const pattern = line.replace(/^-\s*/, "").split("#")[0].trim();
9022
+ if (pattern && filePath.includes(pattern)) {
9023
+ return `FLOWDECK [arch-constraint]: editing "${pattern}" is forbidden by .codebase/CONSTRAINTS.md`;
9024
+ }
9025
+ }
9026
+ } catch {}
9027
+ return null;
8593
9028
  }
8594
- function extractTargetPath2(args) {
8595
- return String(args.path ?? args.file_path ?? args.filename ?? args.filePath ?? "");
9029
+ function checkPhaseEnforcement(directory) {
9030
+ try {
9031
+ const state = readPlanningState(directory);
9032
+ const flowdeckConfig = resolveDesignFirstConfig(loadFlowDeckConfig(directory));
9033
+ if (state.phase > 0 && state.phase < 3) {
9034
+ return `FLOWDECK [phase-gate]: writing to codebase is blocked in phase ${state.phase} (${state.phase === 1 ? "discuss" : "plan"}). Run /fd-plan --confirm to enter execute phase.`;
9035
+ }
9036
+ if (flowdeckConfig.enabled && flowdeckConfig.requireApprovalBeforeImplementation && isUiDesignApprovalRequired(directory)) {
9037
+ if (flowdeckConfig.enforcement === "advisory") {
9038
+ return `FLOWDECK [design-gate]: advisory design-first mode detected missing approval. Run /fd-design --mode=draft or set design_override=true in STATE.md.`;
9039
+ }
9040
+ return `FLOWDECK [design-gate]: UI-heavy task requires approved design handoff before implementation. Run /fd-design --mode=draft and ensure design_stage=handoff_complete + design_approved=true, or set explicit design_override with reason.`;
9041
+ }
9042
+ } catch {}
9043
+ return null;
8596
9044
  }
8597
-
8598
- // src/hooks/approval-hook.ts
8599
- var WRITE_TOOLS = new Set([
8600
- "write_file",
8601
- "edit_file",
8602
- "create_file",
8603
- "apply_patch",
8604
- "str_replace_editor",
8605
- "write",
8606
- "edit"
8607
- ]);
8608
- function allow3(reason) {
8609
- return { verdict: "allow", reason, riskFlags: [], source: "approval-manager" };
9045
+ function isUiDesignApprovalRequired(directory) {
9046
+ const state = readPlanningState(directory);
9047
+ if (state.design_override && state.design_override_reason && state.design_override_reason.trim().length > 0)
9048
+ return false;
9049
+ if (state.requires_design_first) {
9050
+ return !(state.design_stage === "handoff_complete" && state.design_approved);
9051
+ }
9052
+ if (state.task_type && isUiHeavyTask(state.task_type)) {
9053
+ return !(state.design_stage === "handoff_complete" && state.design_approved);
9054
+ }
9055
+ const planPath = phasePlanPath(directory, state.phase || 1);
9056
+ if (!existsSync20(planPath))
9057
+ return false;
9058
+ const planContent = readFileSync20(planPath, "utf-8");
9059
+ if (!isUiHeavyTask(planContent))
9060
+ return false;
9061
+ return !(state.design_stage === "handoff_complete" && state.design_approved);
8610
9062
  }
8611
- function ask(reason, riskFlags = []) {
9063
+ function allow(reason, riskFlags = []) {
9064
+ return { verdict: "allow", reason, riskFlags, source: "tool-guard" };
9065
+ }
9066
+ function deny(reason, escalationMessage, riskFlags = []) {
8612
9067
  return {
8613
- verdict: "ask",
9068
+ verdict: "deny",
8614
9069
  reason,
8615
9070
  riskFlags,
8616
- source: "approval-manager"
9071
+ source: "tool-guard",
9072
+ escalationMessage
8617
9073
  };
8618
9074
  }
8619
- function evaluate3(input) {
9075
+ function evaluate(input) {
8620
9076
  const { directory, tool: tool13, args } = input;
8621
- if (!WRITE_TOOLS.has(tool13)) {
8622
- return allow3("Tool does not require approval");
8623
- }
8624
- const filePath = String(args?.path ?? args?.file_path ?? args?.filename ?? args?.filePath ?? "");
8625
- if (!filePath) {
8626
- return allow3("No target path — approval not required");
9077
+ if (tool13 !== "bash" && tool13 !== "read" && tool13 !== "write" && tool13 !== "edit") {
9078
+ return allow("Tool is not guardable");
8627
9079
  }
8628
- if (!isSensitivePath(filePath)) {
8629
- return allow3("Path is not sensitive");
9080
+ const blocked = isBlocked(tool13, args);
9081
+ if (blocked) {
9082
+ return deny(blocked, blocked, ["dangerous-pattern"]);
8630
9083
  }
8631
- const approval = checkApproval(directory, {
8632
- run_id: input.runId,
8633
- session_id: input.sessionID,
8634
- agent: input.agent,
8635
- file_path: filePath,
8636
- content_hash: computeContentHash(args)
8637
- });
8638
- if (approval) {
8639
- return allow3(`Approved by ${approval.id}`);
9084
+ if (tool13 === "write" || tool13 === "edit") {
9085
+ const phaseBlock = checkPhaseEnforcement(directory);
9086
+ if (phaseBlock) {
9087
+ const isAdvisory = phaseBlock.includes("[design-gate]: advisory");
9088
+ if (isAdvisory) {
9089
+ return allow(phaseBlock, ["design-gate-advisory"]);
9090
+ }
9091
+ return deny(phaseBlock, phaseBlock, ["phase-gate"]);
9092
+ }
9093
+ const filePath = extractTargetPath(args);
9094
+ const constraintBlock = checkArchConstraint(directory, filePath);
9095
+ if (constraintBlock) {
9096
+ return deny(constraintBlock, constraintBlock, ["arch-constraint"]);
9097
+ }
8640
9098
  }
8641
- return ask(`"${filePath}" is a sensitive file (auth/payment/secrets/infra). Manual approval needed before editing.`, ["sensitive-path"]);
9099
+ return allow("Tool guard passed");
8642
9100
  }
8643
9101
 
8644
- // src/services/deadlock-detector.ts
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";
8648
-
8649
- // src/services/agent-trace-graph.ts
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";
8653
- function agentSpansPath(dir) {
8654
- return join23(codebaseDir(dir), "AGENT_SPANS.jsonl");
8655
- }
8656
- function loadAllSpans(dir) {
8657
- const p = agentSpansPath(dir);
8658
- if (!existsSync23(p))
8659
- return [];
8660
- try {
8661
- return readFileSync23(p, "utf-8").trim().split(`
8662
- `).filter(Boolean).map((l) => JSON.parse(l));
8663
- } catch {
8664
- return [];
9102
+ // src/hooks/guard-rails.ts
9103
+ import { existsSync as existsSync21, readFileSync as readFileSync21 } from "fs";
9104
+ import { join as join21 } from "path";
9105
+ var PLANNING_DIR2 = ".planning";
9106
+ var CONFIG_FILE = "config.json";
9107
+ var STATE_FILE2 = "STATE.md";
9108
+ function resolveExecutionMode(configPath, trustScore, volatility) {
9109
+ if (existsSync21(configPath)) {
9110
+ try {
9111
+ const config = JSON.parse(readFileSync21(configPath, "utf-8"));
9112
+ if (config.execution_mode === "review-only")
9113
+ return "review-only";
9114
+ if (config.execution_mode === "guarded")
9115
+ return "guarded";
9116
+ if (config.execution_mode === "auto")
9117
+ return "auto";
9118
+ } catch {}
8665
9119
  }
8666
- }
8667
- function saveAllSpans(dir, spans) {
8668
- const p = agentSpansPath(dir);
8669
- const cd = codebaseDir(dir);
8670
- if (!existsSync23(cd))
8671
- mkdirSync13(cd, { recursive: true });
8672
- writeFileSync14(p, spans.map((s) => JSON.stringify(s)).join(`
8673
- `) + `
8674
- `, "utf-8");
8675
- }
8676
- function openSpan(dir, opts) {
8677
- const cd = codebaseDir(dir);
8678
- if (!existsSync23(cd))
8679
- mkdirSync13(cd, { recursive: true });
8680
- const span = {
8681
- span_id: randomUUID2(),
8682
- trace_id: opts.trace_id,
8683
- parent_span_id: opts.parent_span_id,
8684
- invoker: opts.invoker,
8685
- agent: opts.agent,
8686
- task_description: opts.task_description,
8687
- stage: opts.stage,
8688
- started_at: new Date().toISOString(),
8689
- status: "running",
8690
- output_valid: false,
8691
- contract_violations: [],
8692
- tools_used: [],
8693
- retry_count: 0,
8694
- depth: opts.depth ?? 0,
8695
- model: opts.model
8696
- };
8697
- appendFileSync4(agentSpansPath(dir), JSON.stringify(span) + `
8698
- `, "utf-8");
8699
- return span;
8700
- }
8701
- function closeSpan(dir, span_id, status, opts = {}) {
8702
- const spans = loadAllSpans(dir);
8703
- const idx = spans.findLastIndex((s) => s.span_id === span_id);
8704
- if (idx === -1)
8705
- return;
8706
- const startedMs = new Date(spans[idx].started_at).getTime();
8707
- spans[idx] = {
8708
- ...spans[idx],
8709
- ended_at: new Date().toISOString(),
8710
- status,
8711
- latency_ms: Date.now() - startedMs,
8712
- output_valid: opts.output_valid ?? false,
8713
- contract_violations: opts.contract_violations ?? spans[idx].contract_violations,
8714
- tools_used: opts.tools_used ?? spans[idx].tools_used,
8715
- handoff_payload: opts.handoff_payload,
8716
- cost_estimate: opts.cost_estimate,
8717
- retry_count: opts.retry_count ?? spans[idx].retry_count
8718
- };
8719
- saveAllSpans(dir, spans);
8720
- }
8721
- function recordToolUsed(dir, span_id, toolName) {
8722
- const spans = loadAllSpans(dir);
8723
- const idx = spans.findLastIndex((s) => s.span_id === span_id);
8724
- if (idx === -1)
8725
- return;
8726
- if (!spans[idx].tools_used.includes(toolName)) {
8727
- spans[idx].tools_used = [...spans[idx].tools_used, toolName];
8728
- saveAllSpans(dir, spans);
9120
+ if (trustScore !== null) {
9121
+ if (trustScore < 30)
9122
+ return "review-only";
9123
+ if (trustScore < 60)
9124
+ return "guarded";
8729
9125
  }
9126
+ if (volatility === "critical")
9127
+ return "review-only";
9128
+ if (volatility === "volatile")
9129
+ return "guarded";
9130
+ return "auto";
8730
9131
  }
8731
- function addSpanViolation(dir, span_id, violation) {
8732
- const spans = loadAllSpans(dir);
8733
- const idx = spans.findLastIndex((s) => s.span_id === span_id);
8734
- if (idx === -1)
8735
- return;
8736
- spans[idx].contract_violations = [...spans[idx].contract_violations, violation];
8737
- saveAllSpans(dir, spans);
9132
+ var BUILD_DEPLOY_PATTERNS = [
9133
+ "npm build",
9134
+ "npm run build",
9135
+ "bun build",
9136
+ "yarn build",
9137
+ "npm deploy",
9138
+ "yarn deploy",
9139
+ "bun deploy",
9140
+ "npm install",
9141
+ "yarn install",
9142
+ "bun install",
9143
+ "make build",
9144
+ "make deploy",
9145
+ "docker build",
9146
+ "docker push",
9147
+ "docker-compose",
9148
+ "git push",
9149
+ "git deploy",
9150
+ "gradle build",
9151
+ "mvn package",
9152
+ "ant build",
9153
+ "cargo build",
9154
+ "cargo deploy",
9155
+ "python setup.py",
9156
+ "pip install",
9157
+ "rails deploy",
9158
+ "rake deploy"
9159
+ ];
9160
+ function allow2(reason, riskFlags = []) {
9161
+ return { verdict: "allow", reason, riskFlags, source: "guard-rails" };
8738
9162
  }
8739
- function recordContractViolation(dir, span_id, violation) {
8740
- addSpanViolation(dir, span_id, violation);
9163
+ function deny2(reason, escalationMessage, riskFlags = []) {
9164
+ return {
9165
+ verdict: "deny",
9166
+ reason,
9167
+ riskFlags,
9168
+ source: "guard-rails",
9169
+ escalationMessage
9170
+ };
8741
9171
  }
8742
- function getTraceSpans(dir, trace_id) {
8743
- return loadAllSpans(dir).filter((s) => s.trace_id === trace_id);
9172
+ function evaluate2(input) {
9173
+ const { directory, tool: tool13, args } = input;
9174
+ const planningDirPath = join21(directory, PLANNING_DIR2);
9175
+ const codebaseDirectory = codebaseDir(directory);
9176
+ const configPath = join21(planningDirPath, CONFIG_FILE);
9177
+ const statePath3 = join21(planningDirPath, STATE_FILE2);
9178
+ const workspaceRoot = findWorkspaceRoot(directory);
9179
+ if (workspaceRoot && directory !== workspaceRoot) {
9180
+ const workspaceConfig = getWorkspaceConfig(directory);
9181
+ if (workspaceConfig && workspaceConfig.workspace_mode === "shared" && !existsSync21(planningDirPath)) {
9182
+ const msg = `No .planning/ in this sub-repo. Switch to workspace root: cd ${workspaceRoot}`;
9183
+ return deny2(msg, `[flowdeck] BLOCK: ${msg}`, ["workspace-shared-mode"]);
9184
+ }
9185
+ }
9186
+ if (tool13 === "write" || tool13 === "edit") {
9187
+ if (!existsSync21(planningDirPath)) {
9188
+ return allow2("FlowDeck not initialized in this directory — skipping guard-rails");
9189
+ }
9190
+ if (!existsSync21(codebaseDirectory)) {
9191
+ const msg = ".codebase/ not found. Run /fd-map-codebase to map the codebase.";
9192
+ return allow2(msg, ["codebase-missing"]);
9193
+ }
9194
+ const execMode = resolveExecutionMode(configPath, null);
9195
+ if (execMode === "review-only") {
9196
+ const msg = "review-only mode: propose diff but do not apply. Set execution_mode in .planning/config.json to change.";
9197
+ return deny2(msg, `[flowdeck] BLOCK (${msg})`, ["review-only-mode"]);
9198
+ }
9199
+ if (execMode === "guarded") {
9200
+ return allow2("guarded mode: edit will proceed but flag for human review", ["guarded-mode"]);
9201
+ }
9202
+ const designGateMessage = getDesignGateMessage(directory);
9203
+ if (designGateMessage) {
9204
+ if (designGateMessage.startsWith("[flowdeck] WARNING:")) {
9205
+ return allow2(designGateMessage, ["design-gate-advisory"]);
9206
+ }
9207
+ return deny2(designGateMessage, designGateMessage, ["design-gate"]);
9208
+ }
9209
+ const severity = effectiveSeverity(configPath, statePath3);
9210
+ if (severity === null) {
9211
+ return allow2("guard_enforcement is off");
9212
+ }
9213
+ if (severity === "warn") {
9214
+ const warning = getWarningMessage(planningDirPath);
9215
+ return allow2(`[flowdeck] WARNING: ${warning}`, ["plan-not-confirmed"]);
9216
+ }
9217
+ const blockMessage = getBlockMessage(planningDirPath);
9218
+ return deny2(blockMessage, `[flowdeck] BLOCK: ${blockMessage}`, ["plan-not-confirmed"]);
9219
+ }
9220
+ if (tool13 === "bash") {
9221
+ const cmd = String(args?.command || "");
9222
+ for (const pattern of BUILD_DEPLOY_PATTERNS) {
9223
+ if (cmd.includes(pattern)) {
9224
+ if (!getPlanConfirmed(statePath3)) {
9225
+ const msg = "Build/deploy command detected but plan is not confirmed. Run /fd-plan first.";
9226
+ return allow2(`[flowdeck] WARNING: ${msg}`, ["build-deploy-without-plan"]);
9227
+ }
9228
+ break;
9229
+ }
9230
+ }
9231
+ }
9232
+ return allow2("Guard rails passed");
8744
9233
  }
8745
-
8746
- // src/services/deadlock-detector.ts
8747
- function resolveConfig(directory) {
8748
- try {
8749
- const config = loadFlowDeckConfig(directory);
8750
- const dc = config?.governance?.deadlockDetection;
8751
- return {
8752
- enabled: dc?.enabled ?? true,
8753
- bounceThreshold: dc?.bounceThreshold ?? 3,
8754
- retryLoopThreshold: dc?.retryLoopThreshold ?? 3,
8755
- stageStallMinutes: dc?.stageStallMinutes ?? 30,
8756
- autoStop: dc?.autoStop ?? false
8757
- };
8758
- } catch {
8759
- return { enabled: true, bounceThreshold: 3, retryLoopThreshold: 3, stageStallMinutes: 30, autoStop: false };
9234
+ function getDesignGateMessage(dir) {
9235
+ const designConfig = resolveDesignFirstConfig(loadFlowDeckConfig(dir));
9236
+ if (!designConfig.enabled || !designConfig.requireApprovalBeforeImplementation)
9237
+ return null;
9238
+ const state = readPlanningState(dir);
9239
+ if (state.design_override && state.design_override_reason && state.design_override_reason.trim().length > 0)
9240
+ return null;
9241
+ const designApproved = state.design_stage === "handoff_complete" && state.design_approved;
9242
+ if (state.requires_design_first || state.task_type && isUiHeavyTask(state.task_type) || planSuggestsUiHeavy(dir, state.phase || 1)) {
9243
+ if (designApproved)
9244
+ return null;
9245
+ if (designConfig.enforcement === "advisory") {
9246
+ return "[flowdeck] WARNING: UI-heavy task detected without approved design handoff. Run /fd-design --mode=draft first.";
9247
+ }
9248
+ return "[flowdeck] BLOCK: UI-heavy task requires approved design handoff. Run /fd-design --mode=draft or set explicit design override in STATE.md.";
8760
9249
  }
9250
+ return null;
8761
9251
  }
8762
- function deadlockSignalsPath(dir) {
8763
- return join24(codebaseDir(dir), "DEADLOCK_SIGNALS.jsonl");
9252
+ function planSuggestsUiHeavy(dir, phase) {
9253
+ const planPath = phasePlanPath(dir, phase);
9254
+ if (!existsSync21(planPath))
9255
+ return false;
9256
+ const planContent = readFileSync21(planPath, "utf-8");
9257
+ return isUiHeavyTask(planContent);
8764
9258
  }
8765
- function appendSignal(dir, signal) {
8766
- const cd = codebaseDir(dir);
8767
- if (!existsSync24(cd))
8768
- mkdirSync14(cd, { recursive: true });
8769
- appendFileSync5(deadlockSignalsPath(dir), JSON.stringify(signal) + `
8770
- `, "utf-8");
9259
+ function effectiveSeverity(configPath, statePath3) {
9260
+ if (existsSync21(configPath)) {
9261
+ try {
9262
+ const configContent = readFileSync21(configPath, "utf-8");
9263
+ const config = JSON.parse(configContent);
9264
+ if (config.guard_enforcement === "warn")
9265
+ return "warn";
9266
+ if (config.guard_enforcement === "block")
9267
+ return "block";
9268
+ if (config.guard_enforcement === "off")
9269
+ return null;
9270
+ } catch {}
9271
+ }
9272
+ return getPlanConfirmed(statePath3) ? "block" : "warn";
8771
9273
  }
8772
- function getSignals(dir, trace_id) {
8773
- const p = deadlockSignalsPath(dir);
8774
- if (!existsSync24(p))
8775
- return [];
9274
+ function getPlanConfirmed(statePath3) {
9275
+ if (!existsSync21(statePath3))
9276
+ return false;
8776
9277
  try {
8777
- const all = readFileSync24(p, "utf-8").trim().split(`
8778
- `).filter(Boolean).map((l) => JSON.parse(l));
8779
- return trace_id ? all.filter((s) => s.trace_id === trace_id) : all;
9278
+ const content = readFileSync21(statePath3, "utf-8");
9279
+ const match = content.match(/plan_confirmed:\s*(true|false)/i);
9280
+ return match ? match[1].toLowerCase() === "true" : false;
8780
9281
  } catch {
8781
- return [];
9282
+ return false;
8782
9283
  }
8783
9284
  }
8784
- function detectAgentBounce(dir, trace_id, cfg) {
8785
- const spans = getTraceSpans(dir, trace_id);
8786
- const pairCounts = {};
8787
- for (let i = 1;i < spans.length; i++) {
8788
- const key = `${spans[i - 1].agent}→${spans[i].agent}`;
8789
- pairCounts[key] = (pairCounts[key] ?? 0) + 1;
8790
- }
8791
- for (const [pair, count] of Object.entries(pairCounts)) {
8792
- if (count >= cfg.bounceThreshold) {
8793
- const [a, b] = pair.split("→");
8794
- return {
8795
- signal_id: randomUUID3(),
8796
- trace_id,
8797
- detected_at: new Date().toISOString(),
8798
- type: "agent_bounce",
8799
- evidence: [`Agent pair "${pair}" handed off ${count} times (threshold: ${cfg.bounceThreshold})`],
8800
- agents_involved: [a, b],
8801
- recommended_action: "escalate_human",
8802
- auto_stop: cfg.autoStop
8803
- };
8804
- }
9285
+ function getWarningMessage(planningDir2) {
9286
+ if (!existsSync21(join21(planningDir2, STATE_FILE2))) {
9287
+ return "No STATE.md found. Run /fd-map-codebase then /fd-new-feature to start a feature.";
8805
9288
  }
8806
- return null;
9289
+ return "Plan not confirmed. Run /fd-plan and confirm to enable execution.";
8807
9290
  }
8808
- function detectCircularDelegation(dir, trace_id, cfg) {
8809
- const spans = getTraceSpans(dir, trace_id);
8810
- const graph = {};
8811
- for (const span of spans) {
8812
- if (span.invoker === span.agent)
8813
- continue;
8814
- if (!graph[span.invoker])
8815
- graph[span.invoker] = new Set;
8816
- graph[span.invoker].add(span.agent);
8817
- }
8818
- function findCycle(node, visited2, stack) {
8819
- visited2.add(node);
8820
- for (const neighbor of [...graph[node] ?? []]) {
8821
- if (stack.includes(neighbor))
8822
- return [...stack, neighbor];
8823
- if (!visited2.has(neighbor)) {
8824
- const result = findCycle(neighbor, visited2, [...stack, neighbor]);
8825
- if (result)
8826
- return result;
8827
- }
8828
- }
8829
- return null;
9291
+ function getBlockMessage(planningDir2) {
9292
+ if (!existsSync21(join21(planningDir2, STATE_FILE2))) {
9293
+ return "No STATE.md found. Run /fd-map-codebase then /fd-new-feature to start a feature.";
8830
9294
  }
8831
- const visited = new Set;
8832
- for (const node of Object.keys(graph)) {
8833
- if (!visited.has(node)) {
8834
- const cycle = findCycle(node, visited, [node]);
8835
- if (cycle) {
8836
- return {
8837
- signal_id: randomUUID3(),
8838
- trace_id,
8839
- detected_at: new Date().toISOString(),
8840
- type: "circular_delegation",
8841
- evidence: [`Delegation cycle: ${cycle.join(" → ")}`],
8842
- agents_involved: cycle,
8843
- recommended_action: "stop",
8844
- auto_stop: true
8845
- };
8846
- }
8847
- }
9295
+ return "Plan not confirmed. Run /fd-plan and confirm to enable execution.";
9296
+ }
9297
+
9298
+ // src/services/approval-manager.ts
9299
+ import { existsSync as existsSync22, readFileSync as readFileSync22, writeFileSync as writeFileSync13, mkdirSync as mkdirSync12 } from "fs";
9300
+ import { join as join22 } from "path";
9301
+ import { createHash as createHash4 } from "crypto";
9302
+ import { randomUUID } from "crypto";
9303
+ var APPROVAL_TTL_MS = 30 * 60 * 1000;
9304
+ var SENSITIVE_PATTERNS = [
9305
+ /auth/i,
9306
+ /login/i,
9307
+ /password/i,
9308
+ /secret/i,
9309
+ /token/i,
9310
+ /jwt/i,
9311
+ /session/i,
9312
+ /oauth/i,
9313
+ /payment/i,
9314
+ /billing/i,
9315
+ /stripe/i,
9316
+ /credit/i,
9317
+ /migration/i,
9318
+ /migrate/i,
9319
+ /schema/i,
9320
+ /alembic/i,
9321
+ /infra/i,
9322
+ /terraform/i,
9323
+ /ansible/i,
9324
+ /k8s/i,
9325
+ /kubernetes/i,
9326
+ /docker/i,
9327
+ /\.env/i,
9328
+ /secrets\./i,
9329
+ /config\/prod/i,
9330
+ /production/i,
9331
+ /admin/i,
9332
+ /privilege/i,
9333
+ /sudo/i
9334
+ ];
9335
+ function isSensitivePath(filePath) {
9336
+ return SENSITIVE_PATTERNS.some((p) => p.test(filePath));
9337
+ }
9338
+ function approvalsPath(dir) {
9339
+ return join22(codebaseDir(dir), "APPROVALS.json");
9340
+ }
9341
+ function loadStore(dir) {
9342
+ const p = approvalsPath(dir);
9343
+ if (!existsSync22(p))
9344
+ return { requests: [] };
9345
+ try {
9346
+ return JSON.parse(readFileSync22(p, "utf-8"));
9347
+ } catch {
9348
+ return { requests: [] };
8848
9349
  }
8849
- return null;
8850
9350
  }
8851
- function detectStepRetryLoop(dir, trace_id, cfg) {
8852
- const spans = getTraceSpans(dir, trace_id);
8853
- const stageCounts = {};
8854
- const stageAgents = {};
8855
- for (const span of spans) {
8856
- const key = `${span.agent}:${span.stage}`;
8857
- stageCounts[key] = (stageCounts[key] ?? 0) + 1;
8858
- if (!stageAgents[key])
8859
- stageAgents[key] = new Set;
8860
- stageAgents[key].add(span.agent);
8861
- }
8862
- for (const [key, count] of Object.entries(stageCounts)) {
8863
- if (count >= cfg.retryLoopThreshold) {
8864
- return {
8865
- signal_id: randomUUID3(),
8866
- trace_id,
8867
- detected_at: new Date().toISOString(),
8868
- type: "step_retry_loop",
8869
- evidence: [`Stage "${key}" executed ${count} times (threshold: ${cfg.retryLoopThreshold})`],
8870
- agents_involved: [...stageAgents[key] ?? new Set],
8871
- recommended_action: "escalate_human",
8872
- auto_stop: cfg.autoStop
8873
- };
8874
- }
8875
- }
8876
- return null;
9351
+ function saveStore(dir, store) {
9352
+ const cd = codebaseDir(dir);
9353
+ if (!existsSync22(cd))
9354
+ mkdirSync12(cd, { recursive: true });
9355
+ writeFileSync13(approvalsPath(dir), JSON.stringify(store, null, 2), "utf-8");
8877
9356
  }
8878
- function detectStageStall(dir, trace_id, cfg) {
8879
- const spans = getTraceSpans(dir, trace_id);
9357
+ function requestApproval(dir, run_id, trigger, reason, options = {}) {
9358
+ const store = loadStore(dir);
9359
+ const req = {
9360
+ id: randomUUID(),
9361
+ run_id,
9362
+ session_id: options.session_id ?? "session-0",
9363
+ requested_at: new Date().toISOString(),
9364
+ status: "pending",
9365
+ trigger,
9366
+ reason,
9367
+ risk_score: options.risk_score ?? 0,
9368
+ ...options.agent ? { agent: options.agent } : {},
9369
+ ...options.file_path ? { file_path: options.file_path } : {},
9370
+ ...options.content_hash ? { content_hash: options.content_hash } : {},
9371
+ ...options.change_description ? { change_description: options.change_description } : {}
9372
+ };
9373
+ store.requests.push(req);
9374
+ saveStore(dir, store);
9375
+ return req;
9376
+ }
9377
+ function checkApproval(dir, criteria) {
9378
+ const store = loadStore(dir);
8880
9379
  const now = Date.now();
8881
- for (const span of spans) {
8882
- if (span.status !== "running")
8883
- continue;
8884
- const elapsed = (now - new Date(span.started_at).getTime()) / 1000 / 60;
8885
- if (elapsed >= cfg.stageStallMinutes) {
8886
- return {
8887
- signal_id: randomUUID3(),
8888
- trace_id,
8889
- detected_at: new Date().toISOString(),
8890
- type: "stage_stall",
8891
- evidence: [
8892
- `Agent "${span.agent}" in stage "${span.stage}" running for ${Math.round(elapsed)}min (threshold: ${cfg.stageStallMinutes}min)`
8893
- ],
8894
- agents_involved: [span.agent],
8895
- recommended_action: "escalate_human",
8896
- auto_stop: cfg.autoStop
8897
- };
8898
- }
8899
- }
8900
- return null;
9380
+ return store.requests.filter((r) => r.status === "approved" && r.resolved_at && r.run_id === criteria.run_id && r.session_id === criteria.session_id && r.agent === criteria.agent && r.file_path === criteria.file_path && r.content_hash === criteria.content_hash && now - new Date(r.resolved_at).getTime() < APPROVAL_TTL_MS).sort((a, b) => b.resolved_at.localeCompare(a.resolved_at)).at(0) ?? null;
8901
9381
  }
8902
- function detectDeadlocks(dir, trace_id) {
8903
- const cfg = resolveConfig(dir);
8904
- if (!cfg.enabled)
8905
- return [];
8906
- const existingTypes = new Set(getSignals(dir, trace_id).map((s) => s.type));
8907
- const candidates = [
8908
- detectAgentBounce(dir, trace_id, cfg),
8909
- detectCircularDelegation(dir, trace_id, cfg),
8910
- detectStepRetryLoop(dir, trace_id, cfg),
8911
- detectStageStall(dir, trace_id, cfg)
8912
- ];
8913
- const newSignals = candidates.filter((s) => s !== null && !existingTypes.has(s.type));
8914
- for (const signal of newSignals)
8915
- appendSignal(dir, signal);
8916
- return newSignals;
9382
+ function computeContentHash(args) {
9383
+ const keys = Object.keys(args).sort();
9384
+ const payload = JSON.stringify(args, keys);
9385
+ return createHash4("sha256").update(payload).digest("hex").slice(0, 16);
8917
9386
  }
8918
- function isTraceStuck(dir, trace_id) {
8919
- return getSignals(dir, trace_id).some((s) => s.auto_stop);
9387
+ function requestApprovalForTool(dir, run_id, session_id, agent, tool13, args) {
9388
+ const filePath = extractTargetPath2(args);
9389
+ const isSensitive = filePath ? isSensitivePath(filePath) : false;
9390
+ return requestApproval(dir, run_id, tool13, `Approval required for tool "${tool13}"`, {
9391
+ file_path: filePath,
9392
+ risk_score: isSensitive ? 30 : 50,
9393
+ session_id,
9394
+ agent,
9395
+ content_hash: computeContentHash(args),
9396
+ change_description: `Tool "${tool13}" requested on ${filePath || "unknown target"}`
9397
+ });
9398
+ }
9399
+ function extractTargetPath2(args) {
9400
+ return String(args.path ?? args.file_path ?? args.filename ?? args.filePath ?? "");
8920
9401
  }
8921
9402
 
8922
- // src/services/audit-log.ts
8923
- import {
8924
- existsSync as existsSync25,
8925
- mkdirSync as mkdirSync15,
8926
- appendFileSync as appendFileSync6,
8927
- readFileSync as readFileSync25,
8928
- writeFileSync as writeFileSync15,
8929
- renameSync,
8930
- unlinkSync
8931
- } from "fs";
8932
- import { join as join25 } from "path";
8933
- var AUDIT_FILE = "AUDIT.jsonl";
8934
- var ROTATE_LINE_COUNT = 1000;
8935
- function auditPath(directory) {
8936
- return join25(codebaseDir(directory), AUDIT_FILE);
9403
+ // src/hooks/approval-hook.ts
9404
+ var WRITE_TOOLS = new Set([
9405
+ "write_file",
9406
+ "edit_file",
9407
+ "create_file",
9408
+ "apply_patch",
9409
+ "str_replace_editor",
9410
+ "write",
9411
+ "edit"
9412
+ ]);
9413
+ function allow3(reason) {
9414
+ return { verdict: "allow", reason, riskFlags: [], source: "approval-manager" };
8937
9415
  }
8938
- function ensureDirectory(dir) {
8939
- const base = codebaseDir(dir);
8940
- if (!existsSync25(base)) {
8941
- mkdirSync15(base, { recursive: true });
8942
- }
9416
+ function ask(reason, riskFlags = []) {
9417
+ return {
9418
+ verdict: "ask",
9419
+ reason,
9420
+ riskFlags,
9421
+ source: "approval-manager"
9422
+ };
8943
9423
  }
8944
- function rotateIfNeeded(path, appLog) {
8945
- try {
8946
- const content = readFileSync25(path, "utf-8");
8947
- const lines = content.split(`
8948
- `).filter((line) => line.trim().length > 0);
8949
- if (lines.length <= ROTATE_LINE_COUNT)
8950
- return;
8951
- const backupPath = `${path}.backup`;
8952
- renameSync(path, backupPath);
8953
- const keep = lines.slice(-ROTATE_LINE_COUNT);
8954
- writeFileSync15(path, keep.join(`
8955
- `) + `
8956
- `, "utf-8");
8957
- try {
8958
- unlinkSync(backupPath);
8959
- } catch {}
8960
- } catch (error) {
8961
- if (appLog) {
8962
- const message = error instanceof Error ? error.message : String(error);
8963
- appLog(`[audit-log] rotation failed: ${message}`);
8964
- }
9424
+ function evaluate3(input) {
9425
+ const { directory, tool: tool13, args } = input;
9426
+ if (!WRITE_TOOLS.has(tool13)) {
9427
+ return allow3("Tool does not require approval");
9428
+ }
9429
+ const filePath = String(args?.path ?? args?.file_path ?? args?.filename ?? args?.filePath ?? "");
9430
+ if (!filePath) {
9431
+ return allow3("No target path — approval not required");
9432
+ }
9433
+ if (!isSensitivePath(filePath)) {
9434
+ return allow3("Path is not sensitive");
9435
+ }
9436
+ const approval = checkApproval(directory, {
9437
+ run_id: input.runId,
9438
+ session_id: input.sessionID,
9439
+ agent: input.agent,
9440
+ file_path: filePath,
9441
+ content_hash: computeContentHash(args)
9442
+ });
9443
+ if (approval) {
9444
+ return allow3(`Approved by ${approval.id}`);
8965
9445
  }
9446
+ return ask(`"${filePath}" is a sensitive file (auth/payment/secrets/infra). Manual approval needed before editing.`, ["sensitive-path"]);
8966
9447
  }
8967
- function appendAuditEntry(directory, entry, appLog) {
8968
- ensureDirectory(directory);
8969
- const path = auditPath(directory);
8970
- appendFileSync6(path, JSON.stringify(entry) + `
8971
- `, "utf-8");
8972
- rotateIfNeeded(path, appLog);
9448
+
9449
+ // src/services/deadlock-detector.ts
9450
+ import { existsSync as existsSync24, readFileSync as readFileSync24, appendFileSync as appendFileSync5, mkdirSync as mkdirSync14 } from "fs";
9451
+ import { join as join24 } from "path";
9452
+ import { randomUUID as randomUUID3 } from "crypto";
9453
+
9454
+ // src/services/agent-trace-graph.ts
9455
+ import { existsSync as existsSync23, readFileSync as readFileSync23, appendFileSync as appendFileSync4, writeFileSync as writeFileSync14, mkdirSync as mkdirSync13 } from "fs";
9456
+ import { join as join23 } from "path";
9457
+ import { randomUUID as randomUUID2 } from "crypto";
9458
+ function agentSpansPath(dir) {
9459
+ return join23(codebaseDir(dir), "AGENT_SPANS.jsonl");
8973
9460
  }
8974
- function queryAudit(directory, filter) {
8975
- const path = auditPath(directory);
8976
- if (!existsSync25(path))
9461
+ function loadAllSpans(dir) {
9462
+ const p = agentSpansPath(dir);
9463
+ if (!existsSync23(p))
8977
9464
  return [];
8978
9465
  try {
8979
- const lines = readFileSync25(path, "utf-8").split(`
8980
- `).filter((line) => line.trim().length > 0);
8981
- const results = [];
8982
- for (const line of lines) {
8983
- try {
8984
- const entry = JSON.parse(line);
8985
- if (filter.run_id && entry.run_id !== filter.run_id)
8986
- continue;
8987
- if (filter.session_id && entry.session_id !== filter.session_id)
8988
- continue;
8989
- if (filter.tool && entry.tool !== filter.tool)
8990
- continue;
8991
- if (filter.verdict && entry.verdict !== filter.verdict)
8992
- continue;
8993
- results.push(entry);
8994
- } catch {}
8995
- }
8996
- return results;
9466
+ return readFileSync23(p, "utf-8").trim().split(`
9467
+ `).filter(Boolean).map((l) => JSON.parse(l));
8997
9468
  } catch {
8998
9469
  return [];
8999
9470
  }
9000
9471
  }
9001
- function createAuditLog(directory, appLog) {
9002
- return {
9003
- append: (entry) => appendAuditEntry(directory, entry, appLog),
9004
- query: (filter) => queryAudit(directory, filter)
9005
- };
9006
- }
9007
-
9008
- // src/services/harness-policy.ts
9009
- function allow4(reason, source, riskFlags = []) {
9010
- return { verdict: "allow", reason, riskFlags, source };
9472
+ function saveAllSpans(dir, spans) {
9473
+ const p = agentSpansPath(dir);
9474
+ const cd = codebaseDir(dir);
9475
+ if (!existsSync23(cd))
9476
+ mkdirSync13(cd, { recursive: true });
9477
+ writeFileSync14(p, spans.map((s) => JSON.stringify(s)).join(`
9478
+ `) + `
9479
+ `, "utf-8");
9011
9480
  }
9012
- function deny3(reason, source, escalationMessage, riskFlags = []) {
9013
- return {
9014
- verdict: "deny",
9015
- reason,
9016
- riskFlags,
9017
- source,
9018
- escalationMessage
9481
+ function openSpan(dir, opts) {
9482
+ const cd = codebaseDir(dir);
9483
+ if (!existsSync23(cd))
9484
+ mkdirSync13(cd, { recursive: true });
9485
+ const span = {
9486
+ span_id: randomUUID2(),
9487
+ trace_id: opts.trace_id,
9488
+ parent_span_id: opts.parent_span_id,
9489
+ invoker: opts.invoker,
9490
+ agent: opts.agent,
9491
+ task_description: opts.task_description,
9492
+ stage: opts.stage,
9493
+ started_at: new Date().toISOString(),
9494
+ status: "running",
9495
+ output_valid: false,
9496
+ contract_violations: [],
9497
+ tools_used: [],
9498
+ retry_count: 0,
9499
+ depth: opts.depth ?? 0,
9500
+ model: opts.model
9019
9501
  };
9502
+ appendFileSync4(agentSpansPath(dir), JSON.stringify(span) + `
9503
+ `, "utf-8");
9504
+ return span;
9020
9505
  }
9021
- function ask2(reason, source, approvalRequestId, riskFlags = []) {
9022
- return {
9023
- verdict: "ask",
9024
- reason,
9025
- riskFlags,
9026
- source,
9027
- approvalRequestId
9506
+ function closeSpan(dir, span_id, status, opts = {}) {
9507
+ const spans = loadAllSpans(dir);
9508
+ const idx = spans.findLastIndex((s) => s.span_id === span_id);
9509
+ if (idx === -1)
9510
+ return;
9511
+ const startedMs = new Date(spans[idx].started_at).getTime();
9512
+ spans[idx] = {
9513
+ ...spans[idx],
9514
+ ended_at: new Date().toISOString(),
9515
+ status,
9516
+ latency_ms: Date.now() - startedMs,
9517
+ output_valid: opts.output_valid ?? false,
9518
+ contract_violations: opts.contract_violations ?? spans[idx].contract_violations,
9519
+ tools_used: opts.tools_used ?? spans[idx].tools_used,
9520
+ handoff_payload: opts.handoff_payload,
9521
+ cost_estimate: opts.cost_estimate,
9522
+ retry_count: opts.retry_count ?? spans[idx].retry_count
9028
9523
  };
9524
+ saveAllSpans(dir, spans);
9029
9525
  }
9030
- function validationToDecision(result) {
9031
- const riskFlags = result.violations.map((v) => v.rule);
9032
- if (result.action === "block") {
9033
- return deny3(result.message ?? "Agent contract violation", "agent-contract", result.message ?? "This tool call violates the agent's capability contract", riskFlags);
9034
- }
9035
- if (result.action === "escalate") {
9036
- return ask2(result.message ?? "Agent contract requires escalation", "agent-contract", undefined, riskFlags);
9037
- }
9038
- if (result.action === "warn") {
9039
- return allow4(result.message ?? "Agent contract advisory", "agent-contract", riskFlags);
9526
+ function recordToolUsed(dir, span_id, toolName) {
9527
+ const spans = loadAllSpans(dir);
9528
+ const idx = spans.findLastIndex((s) => s.span_id === span_id);
9529
+ if (idx === -1)
9530
+ return;
9531
+ if (!spans[idx].tools_used.includes(toolName)) {
9532
+ spans[idx].tools_used = [...spans[idx].tools_used, toolName];
9533
+ saveAllSpans(dir, spans);
9040
9534
  }
9041
- return allow4("Agent contract passed", "agent-contract");
9042
9535
  }
9043
- function buildAuditEntry(input, decision) {
9044
- return {
9045
- id: randomUUID4(),
9046
- timestamp: new Date().toISOString(),
9047
- run_id: input.runId,
9048
- session_id: input.sessionID,
9049
- agent: input.agent,
9050
- tool: input.tool,
9051
- command: input.command,
9052
- verdict: decision.verdict,
9053
- reason: decision.reason,
9054
- risk_flags: decision.riskFlags,
9055
- source: decision.source,
9056
- approval_request_id: decision.approvalRequestId
9057
- };
9536
+ function addSpanViolation(dir, span_id, violation) {
9537
+ const spans = loadAllSpans(dir);
9538
+ const idx = spans.findLastIndex((s) => s.span_id === span_id);
9539
+ if (idx === -1)
9540
+ return;
9541
+ spans[idx].contract_violations = [...spans[idx].contract_violations, violation];
9542
+ saveAllSpans(dir, spans);
9058
9543
  }
9059
- function createHarnessPolicy(directory, appLog) {
9060
- const config = loadFlowDeckConfig(directory);
9061
- const auditLog = createAuditLog(directory, appLog);
9062
- const loopDetector = new LoopDetector({
9063
- enabled: config.governance?.loopDetection?.enabled ?? true,
9064
- maxRepeats: config.governance?.loopDetection?.maxRepeats ?? 2,
9065
- similarityThreshold: config.governance?.loopDetection?.similarityThreshold ?? 0.9,
9066
- historySize: config.governance?.loopDetection?.historySize ?? 20
9067
- }, appLog);
9068
- function appendAudit(input, decision) {
9069
- try {
9070
- auditLog.append(buildAuditEntry(input, decision));
9071
- } catch (error) {
9072
- if (appLog) {
9073
- const message = error instanceof Error ? error.message : String(error);
9074
- appLog(`[harness-policy] audit append failed: ${message}`);
9075
- }
9076
- }
9077
- }
9078
- function checkRuntimeLimits(input) {
9079
- try {
9080
- const loop = loopDetector.checkBefore(input.tool, input.args, input.sessionID);
9081
- if (loop.action === "block") {
9082
- return deny3(loop.reason, "loop-detector", loop.escalationMessage, ["loop-detected"]);
9083
- }
9084
- if (loop.action === "warn") {
9085
- return allow4(loop.message, "loop-detector", ["loop-warning"]);
9086
- }
9087
- if (isTraceStuck(input.directory, input.runId)) {
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"]);
9089
- }
9090
- return allow4("Runtime limits passed", "runtime-limits");
9091
- } catch (error) {
9092
- const message = error instanceof Error ? error.message : String(error);
9093
- if (appLog)
9094
- appLog(`[harness-policy] runtime limits error: ${message}`);
9095
- return deny3(`Runtime limits check failed: ${message}`, "runtime-limits", `Runtime limits check failed: ${message}`, ["runtime-check-error"]);
9096
- }
9097
- }
9098
- function checkAgentContract(input) {
9099
- try {
9100
- const result = validateToolAccess(input.directory, input.agent, input.tool, { run_id: input.runId, session_id: input.sessionID });
9101
- return validationToDecision(result);
9102
- } catch (error) {
9103
- const message = error instanceof Error ? error.message : String(error);
9104
- if (appLog)
9105
- appLog(`[harness-policy] agent contract error: ${message}`);
9106
- return deny3(`Agent contract check failed: ${message}`, "agent-contract", `Agent contract check failed: ${message}`, ["agent-contract-error"]);
9107
- }
9108
- }
9109
- function evaluatePolicyEngine(input) {
9110
- try {
9111
- const store = readStore2(input.directory);
9112
- const filePath = extractTargetPath(input.args);
9113
- const active = store.policies.filter((p) => p.active);
9114
- for (const policy of active) {
9115
- const trigger = policy.trigger.toLowerCase();
9116
- if (!trigger.trim())
9117
- continue;
9118
- if (input.tool.toLowerCase().includes(trigger) || filePath.toLowerCase().includes(trigger)) {
9119
- return deny3(`Active policy violation: ${policy.name}`, "policy-engine", policy.rule, ["policy-violation", policy.id]);
9120
- }
9121
- }
9122
- return allow4("No active policy violations", "policy-engine");
9123
- } catch (error) {
9124
- const message = error instanceof Error ? error.message : String(error);
9125
- if (appLog)
9126
- appLog(`[harness-policy] policy engine error: ${message}`);
9127
- return allow4(`Policy engine check failed: ${message}`, "policy-engine", [
9128
- "policy-engine-error"
9129
- ]);
9130
- }
9131
- }
9132
- function checkGovernance(input) {
9133
- try {
9134
- const riskFlags = [];
9135
- if (config.governance?.toolGuard !== false) {
9136
- const tg = evaluate(input);
9137
- if (tg.verdict === "deny")
9138
- return tg;
9139
- riskFlags.push(...tg.riskFlags);
9140
- }
9141
- if (config.governance?.guardRails !== false) {
9142
- const gr = evaluate2(input);
9143
- if (gr.verdict === "deny")
9144
- return gr;
9145
- riskFlags.push(...gr.riskFlags);
9146
- }
9147
- if (config.governance?.approvals !== false) {
9148
- const ap = evaluate3(input);
9149
- if (ap.verdict !== "allow")
9150
- return ap;
9151
- }
9152
- const pe = evaluatePolicyEngine(input);
9153
- if (pe.verdict === "deny")
9154
- return pe;
9155
- riskFlags.push(...pe.riskFlags);
9156
- return allow4("Governance checks passed", "governance", riskFlags);
9157
- } catch (error) {
9158
- const message = error instanceof Error ? error.message : String(error);
9159
- if (appLog)
9160
- appLog(`[harness-policy] governance error: ${message}`);
9161
- return deny3(`Governance check failed: ${message}`, "governance", `Governance check failed: ${message}`, ["governance-error"]);
9162
- }
9544
+ function recordContractViolation(dir, span_id, violation) {
9545
+ addSpanViolation(dir, span_id, violation);
9546
+ }
9547
+ function getTraceSpans(dir, trace_id) {
9548
+ return loadAllSpans(dir).filter((s) => s.trace_id === trace_id);
9549
+ }
9550
+
9551
+ // src/services/deadlock-detector.ts
9552
+ function resolveConfig(directory) {
9553
+ try {
9554
+ const config = loadFlowDeckConfig(directory);
9555
+ const dc = config?.governance?.deadlockDetection;
9556
+ return {
9557
+ enabled: dc?.enabled ?? true,
9558
+ bounceThreshold: dc?.bounceThreshold ?? 3,
9559
+ retryLoopThreshold: dc?.retryLoopThreshold ?? 3,
9560
+ stageStallMinutes: dc?.stageStallMinutes ?? 30,
9561
+ autoStop: dc?.autoStop ?? false
9562
+ };
9563
+ } catch {
9564
+ return { enabled: true, bounceThreshold: 3, retryLoopThreshold: 3, stageStallMinutes: 30, autoStop: false };
9163
9565
  }
9164
- function checkSupervisor(input, _targetName) {
9165
- if (config.governance?.supervisor?.enabled === false) {
9166
- return allow4("Supervisor disabled", "supervisor");
9167
- }
9168
- try {
9169
- return reviewToolCall(input.directory, input);
9170
- } catch (error) {
9171
- const message = error instanceof Error ? error.message : String(error);
9172
- if (appLog)
9173
- appLog(`[harness-policy] supervisor error: ${message}`);
9174
- return deny3(`Supervisor review failed: ${message}`, "supervisor", `Supervisor review failed: ${message}`, ["supervisor-error"]);
9175
- }
9566
+ }
9567
+ function deadlockSignalsPath(dir) {
9568
+ return join24(codebaseDir(dir), "DEADLOCK_SIGNALS.jsonl");
9569
+ }
9570
+ function appendSignal(dir, signal) {
9571
+ const cd = codebaseDir(dir);
9572
+ if (!existsSync24(cd))
9573
+ mkdirSync14(cd, { recursive: true });
9574
+ appendFileSync5(deadlockSignalsPath(dir), JSON.stringify(signal) + `
9575
+ `, "utf-8");
9576
+ }
9577
+ function getSignals(dir, trace_id) {
9578
+ const p = deadlockSignalsPath(dir);
9579
+ if (!existsSync24(p))
9580
+ return [];
9581
+ try {
9582
+ const all = readFileSync24(p, "utf-8").trim().split(`
9583
+ `).filter(Boolean).map((l) => JSON.parse(l));
9584
+ return trace_id ? all.filter((s) => s.trace_id === trace_id) : all;
9585
+ } catch {
9586
+ return [];
9176
9587
  }
9177
- function generateApprovalRequestId(input) {
9178
- try {
9179
- const req = requestApprovalForTool(input.directory, input.runId, input.sessionID, input.agent, input.tool, input.args);
9180
- return req.id;
9181
- } catch (error) {
9182
- const message = error instanceof Error ? error.message : String(error);
9183
- if (appLog)
9184
- appLog(`[harness-policy] approval request failed: ${message}`);
9185
- return;
9186
- }
9588
+ }
9589
+ function detectAgentBounce(dir, trace_id, cfg) {
9590
+ const spans = getTraceSpans(dir, trace_id);
9591
+ const pairCounts = {};
9592
+ for (let i = 1;i < spans.length; i++) {
9593
+ const key = `${spans[i - 1].agent}→${spans[i].agent}`;
9594
+ pairCounts[key] = (pairCounts[key] ?? 0) + 1;
9187
9595
  }
9188
- function combineDecisions(decisions, input) {
9189
- const denied = decisions.find((d) => d.verdict === "deny");
9190
- if (denied)
9191
- return denied;
9192
- const asked = decisions.find((d) => d.verdict === "ask");
9193
- if (asked) {
9596
+ for (const [pair, count] of Object.entries(pairCounts)) {
9597
+ if (count >= cfg.bounceThreshold) {
9598
+ const [a, b] = pair.split("→");
9194
9599
  return {
9195
- ...asked,
9196
- approvalRequestId: asked.approvalRequestId ?? generateApprovalRequestId(input)
9600
+ signal_id: randomUUID3(),
9601
+ trace_id,
9602
+ detected_at: new Date().toISOString(),
9603
+ type: "agent_bounce",
9604
+ evidence: [`Agent pair "${pair}" handed off ${count} times (threshold: ${cfg.bounceThreshold})`],
9605
+ agents_involved: [a, b],
9606
+ recommended_action: "escalate_human",
9607
+ auto_stop: cfg.autoStop
9197
9608
  };
9198
9609
  }
9199
- const riskFlags = decisions.flatMap((d) => d.riskFlags);
9200
- const source = decisions.findLast((d) => d.source !== "harness.policy")?.source ?? "harness.policy";
9201
- return allow4("All policy checks passed", source, riskFlags);
9202
9610
  }
9203
- function evaluate4(input) {
9204
- if (config.harness?.enabled === false) {
9205
- const envDisabled = process.env.FLOWDECK_HARNESS_DISABLED === "1";
9206
- if (!envDisabled) {
9207
- if (appLog) {
9208
- appLog("[harness-policy] SECURITY WARNING: harness.enabled=false without FLOWDECK_HARNESS_DISABLED=1. Enforcing minimal safety checks (loop detection, tool-guard dangerous patterns, orchestrator contract).");
9209
- }
9210
- const runtime2 = checkRuntimeLimits(input);
9211
- if (runtime2.verdict === "deny") {
9212
- appendAudit(input, runtime2);
9213
- return runtime2;
9214
- }
9215
- if (config.governance?.toolGuard !== false) {
9216
- const tg = evaluate(input);
9217
- if (tg.verdict === "deny") {
9218
- appendAudit(input, tg);
9219
- return tg;
9220
- }
9221
- }
9222
- const decision2 = allow4("Harness disabled — minimal safety checks passed", "harness.policy", ["harness-disabled-minimal"]);
9223
- appendAudit(input, decision2);
9224
- return decision2;
9611
+ return null;
9612
+ }
9613
+ function detectCircularDelegation(dir, trace_id, cfg) {
9614
+ const spans = getTraceSpans(dir, trace_id);
9615
+ const graph = {};
9616
+ for (const span of spans) {
9617
+ if (span.invoker === span.agent)
9618
+ continue;
9619
+ if (!graph[span.invoker])
9620
+ graph[span.invoker] = new Set;
9621
+ graph[span.invoker].add(span.agent);
9622
+ }
9623
+ function findCycle(node, visited2, stack) {
9624
+ visited2.add(node);
9625
+ for (const neighbor of [...graph[node] ?? []]) {
9626
+ if (stack.includes(neighbor))
9627
+ return [...stack, neighbor];
9628
+ if (!visited2.has(neighbor)) {
9629
+ const result = findCycle(neighbor, visited2, [...stack, neighbor]);
9630
+ if (result)
9631
+ return result;
9225
9632
  }
9226
- const decision = allow4("Harness disabled by config and env override", "harness.policy");
9227
- appendAudit(input, decision);
9228
- return decision;
9229
- }
9230
- const decisions = [];
9231
- const runtime = checkRuntimeLimits(input);
9232
- decisions.push(runtime);
9233
- if (runtime.verdict === "deny") {
9234
- appendAudit(input, runtime);
9235
- return runtime;
9236
- }
9237
- const contract = checkAgentContract(input);
9238
- decisions.push(contract);
9239
- if (contract.verdict === "deny") {
9240
- appendAudit(input, contract);
9241
- return contract;
9242
9633
  }
9243
- const governance = checkGovernance(input);
9244
- decisions.push(governance);
9245
- if (governance.verdict === "deny") {
9246
- appendAudit(input, governance);
9247
- return governance;
9634
+ return null;
9635
+ }
9636
+ const visited = new Set;
9637
+ for (const node of Object.keys(graph)) {
9638
+ if (!visited.has(node)) {
9639
+ const cycle = findCycle(node, visited, [node]);
9640
+ if (cycle) {
9641
+ return {
9642
+ signal_id: randomUUID3(),
9643
+ trace_id,
9644
+ detected_at: new Date().toISOString(),
9645
+ type: "circular_delegation",
9646
+ evidence: [`Delegation cycle: ${cycle.join(" → ")}`],
9647
+ agents_involved: cycle,
9648
+ recommended_action: "stop",
9649
+ auto_stop: true
9650
+ };
9651
+ }
9248
9652
  }
9249
- const supervisor = checkSupervisor(input, input.agent);
9250
- decisions.push(supervisor);
9251
- const final = combineDecisions(decisions, input);
9252
- appendAudit(input, final);
9253
- return final;
9254
9653
  }
9255
- return {
9256
- evaluate: evaluate4,
9257
- checkAgentContract,
9258
- checkRuntimeLimits,
9259
- checkGovernance,
9260
- checkSupervisor
9261
- };
9654
+ return null;
9262
9655
  }
9263
-
9264
- // src/services/harness-controller.ts
9265
- import { randomUUID as randomUUID7 } from "crypto";
9266
-
9267
- // src/services/preflight-explorer.ts
9268
- var QUESTION_KIND_PATTERNS = [
9269
- {
9270
- kind: "what-tech-stack",
9271
- patterns: ["tech stack", "language", "framework", "what are you using", "what tech", "built with", "written in"]
9272
- },
9273
- {
9274
- kind: "is-project-initialized",
9275
- patterns: ["initialized", "set up", "codebase mapped", "map-codebase", "new feature"]
9276
- },
9277
- {
9278
- kind: "what-is-current-phase",
9279
- patterns: ["current phase", "which phase", "what phase", "where are we", "current state"]
9280
- },
9281
- {
9282
- kind: "what-patterns-exist",
9283
- patterns: ["existing pattern", "how is it done", "how does the codebase", "pattern used", "architecture"]
9284
- },
9285
- {
9286
- kind: "is-ui-heavy",
9287
- patterns: ["ui", "frontend", "user interface", "webpage", "web app", "dashboard", "landing page", "screen"]
9288
- },
9289
- {
9290
- kind: "has-existing-tests",
9291
- patterns: ["test", "spec", "coverage", "tdd", "regression"]
9292
- },
9293
- {
9294
- kind: "has-existing-docs",
9295
- patterns: ["docs", "documentation", "readme", "api docs"]
9296
- },
9297
- {
9298
- kind: "has-ci-cd",
9299
- patterns: ["ci/cd", "continuous integration", "deploy", "pipeline", "github actions", ".github/workflow"]
9300
- },
9301
- {
9302
- kind: "what-agents-available",
9303
- patterns: ["which agent", "available agent", "what agent"]
9304
- },
9305
- {
9306
- kind: "what-commands-available",
9307
- patterns: ["which command", "available command", "what command", "slash command"]
9308
- },
9309
- {
9310
- kind: "what-skills-available",
9311
- patterns: ["skill", "available skill"]
9312
- },
9313
- {
9314
- kind: "has-prior-decisions",
9315
- patterns: ["prior decision", "previous discussion", "what was decided", "earlier session", "previous phase"]
9316
- },
9317
- {
9318
- kind: "has-governance",
9319
- patterns: ["governance", "policy", "approval", "supervisor"]
9656
+ function detectStepRetryLoop(dir, trace_id, cfg) {
9657
+ const spans = getTraceSpans(dir, trace_id);
9658
+ const stageCounts = {};
9659
+ const stageAgents = {};
9660
+ for (const span of spans) {
9661
+ const key = `${span.agent}:${span.stage}`;
9662
+ stageCounts[key] = (stageCounts[key] ?? 0) + 1;
9663
+ if (!stageAgents[key])
9664
+ stageAgents[key] = new Set;
9665
+ stageAgents[key].add(span.agent);
9320
9666
  }
9321
- ];
9322
- function shouldSuppressQuestion(question, result, sessionHistory) {
9323
- const lower = question.toLowerCase();
9324
- const alreadyAsked = sessionHistory.some((h) => h.toLowerCase().trim() === lower.trim());
9325
- if (alreadyAsked) {
9326
- return {
9327
- suppress: true,
9328
- reason: "This question was already asked in the current session."
9329
- };
9667
+ for (const [key, count] of Object.entries(stageCounts)) {
9668
+ if (count >= cfg.retryLoopThreshold) {
9669
+ return {
9670
+ signal_id: randomUUID3(),
9671
+ trace_id,
9672
+ detected_at: new Date().toISOString(),
9673
+ type: "step_retry_loop",
9674
+ evidence: [`Stage "${key}" executed ${count} times (threshold: ${cfg.retryLoopThreshold})`],
9675
+ agents_involved: [...stageAgents[key] ?? new Set],
9676
+ recommended_action: "escalate_human",
9677
+ auto_stop: cfg.autoStop
9678
+ };
9679
+ }
9330
9680
  }
9331
- const matchedEvidence = result.evidenceItems.filter((ev) => {
9332
- const qKind = classifyQuestionKind(lower);
9333
- return qKind !== null && qKind === ev.answersQuestion;
9334
- });
9335
- if (matchedEvidence.length > 0) {
9336
- return {
9337
- suppress: true,
9338
- answeredByEvidence: true,
9339
- reason: `Answered by repo evidence: ${matchedEvidence.map((e) => e.summary).join("; ")}`,
9340
- evidence: matchedEvidence
9341
- };
9681
+ return null;
9682
+ }
9683
+ function detectStageStall(dir, trace_id, cfg) {
9684
+ const spans = getTraceSpans(dir, trace_id);
9685
+ const now = Date.now();
9686
+ for (const span of spans) {
9687
+ if (span.status !== "running")
9688
+ continue;
9689
+ const elapsed = (now - new Date(span.started_at).getTime()) / 1000 / 60;
9690
+ if (elapsed >= cfg.stageStallMinutes) {
9691
+ return {
9692
+ signal_id: randomUUID3(),
9693
+ trace_id,
9694
+ detected_at: new Date().toISOString(),
9695
+ type: "stage_stall",
9696
+ evidence: [
9697
+ `Agent "${span.agent}" in stage "${span.stage}" running for ${Math.round(elapsed)}min (threshold: ${cfg.stageStallMinutes}min)`
9698
+ ],
9699
+ agents_involved: [span.agent],
9700
+ recommended_action: "escalate_human",
9701
+ auto_stop: cfg.autoStop
9702
+ };
9703
+ }
9342
9704
  }
9343
- return { suppress: false };
9705
+ return null;
9344
9706
  }
9345
- function refineClassification(clarificationPrompt, result, sessionHistory) {
9346
- const promptLower = clarificationPrompt.toLowerCase();
9347
- const isTaskTypeQuestion = result.hasProjectMD && (promptLower.includes("new feature") || promptLower.includes("bug fix") || promptLower.includes("ui change") || promptLower.includes("documentation") || promptLower.includes("describe the task") || promptLower.includes("more detail"));
9348
- if (isTaskTypeQuestion) {
9349
- return {
9350
- clarificationStillNeeded: false,
9351
- resolvedReason: "PROJECT.md exists — project is initialized. Defaulting ambiguous task to feature."
9352
- };
9707
+ function detectDeadlocks(dir, trace_id) {
9708
+ const cfg = resolveConfig(dir);
9709
+ if (!cfg.enabled)
9710
+ return [];
9711
+ const existingTypes = new Set(getSignals(dir, trace_id).map((s) => s.type));
9712
+ const candidates = [
9713
+ detectAgentBounce(dir, trace_id, cfg),
9714
+ detectCircularDelegation(dir, trace_id, cfg),
9715
+ detectStepRetryLoop(dir, trace_id, cfg),
9716
+ detectStageStall(dir, trace_id, cfg)
9717
+ ];
9718
+ const newSignals = candidates.filter((s) => s !== null && !existingTypes.has(s.type));
9719
+ for (const signal of newSignals)
9720
+ appendSignal(dir, signal);
9721
+ return newSignals;
9722
+ }
9723
+ function isTraceStuck(dir, trace_id) {
9724
+ return getSignals(dir, trace_id).some((s) => s.auto_stop);
9725
+ }
9726
+
9727
+ // src/services/audit-log.ts
9728
+ import {
9729
+ existsSync as existsSync25,
9730
+ mkdirSync as mkdirSync15,
9731
+ appendFileSync as appendFileSync6,
9732
+ readFileSync as readFileSync25,
9733
+ writeFileSync as writeFileSync15,
9734
+ renameSync,
9735
+ unlinkSync
9736
+ } from "fs";
9737
+ import { join as join25 } from "path";
9738
+ var AUDIT_FILE = "AUDIT.jsonl";
9739
+ var ROTATE_LINE_COUNT = 1000;
9740
+ function auditPath(directory) {
9741
+ return join25(codebaseDir(directory), AUDIT_FILE);
9742
+ }
9743
+ function ensureDirectory(dir) {
9744
+ const base = codebaseDir(dir);
9745
+ if (!existsSync25(base)) {
9746
+ mkdirSync15(base, { recursive: true });
9353
9747
  }
9354
- const suppress = shouldSuppressQuestion(clarificationPrompt, result, sessionHistory);
9355
- if (suppress.suppress) {
9356
- return {
9357
- clarificationStillNeeded: false,
9358
- resolvedReason: suppress.reason
9359
- };
9748
+ }
9749
+ function rotateIfNeeded(path, appLog) {
9750
+ try {
9751
+ const content = readFileSync25(path, "utf-8");
9752
+ const lines = content.split(`
9753
+ `).filter((line) => line.trim().length > 0);
9754
+ if (lines.length <= ROTATE_LINE_COUNT)
9755
+ return;
9756
+ const backupPath = `${path}.backup`;
9757
+ renameSync(path, backupPath);
9758
+ const keep = lines.slice(-ROTATE_LINE_COUNT);
9759
+ writeFileSync15(path, keep.join(`
9760
+ `) + `
9761
+ `, "utf-8");
9762
+ try {
9763
+ unlinkSync(backupPath);
9764
+ } catch {}
9765
+ } catch (error) {
9766
+ if (appLog) {
9767
+ const message = error instanceof Error ? error.message : String(error);
9768
+ appLog(`[audit-log] rotation failed: ${message}`);
9769
+ }
9360
9770
  }
9361
- const lines = [];
9362
- if (result.hasProjectMD)
9363
- lines.push("PROJECT.md is present (project is initialized).");
9364
- if (result.hasStateMD)
9365
- lines.push("STATE.md is present (project has active session).");
9366
- if (result.hasPriorDiscussions)
9367
- lines.push("Prior DISCUSS.md files exist.");
9368
- if (result.techStack.length > 0)
9369
- lines.push(`Tech stack: ${result.techStack.join(", ")}.`);
9370
- if (result.implementationPatterns.length > 0) {
9371
- lines.push(`Implementation patterns: ${result.implementationPatterns.join(", ")}.`);
9771
+ }
9772
+ function appendAuditEntry(directory, entry, appLog) {
9773
+ ensureDirectory(directory);
9774
+ const path = auditPath(directory);
9775
+ appendFileSync6(path, JSON.stringify(entry) + `
9776
+ `, "utf-8");
9777
+ rotateIfNeeded(path, appLog);
9778
+ }
9779
+ function queryAudit(directory, filter) {
9780
+ const path = auditPath(directory);
9781
+ if (!existsSync25(path))
9782
+ return [];
9783
+ try {
9784
+ const lines = readFileSync25(path, "utf-8").split(`
9785
+ `).filter((line) => line.trim().length > 0);
9786
+ const results = [];
9787
+ for (const line of lines) {
9788
+ try {
9789
+ const entry = JSON.parse(line);
9790
+ if (filter.run_id && entry.run_id !== filter.run_id)
9791
+ continue;
9792
+ if (filter.session_id && entry.session_id !== filter.session_id)
9793
+ continue;
9794
+ if (filter.tool && entry.tool !== filter.tool)
9795
+ continue;
9796
+ if (filter.verdict && entry.verdict !== filter.verdict)
9797
+ continue;
9798
+ results.push(entry);
9799
+ } catch {}
9800
+ }
9801
+ return results;
9802
+ } catch {
9803
+ return [];
9372
9804
  }
9805
+ }
9806
+ function createAuditLog(directory, appLog) {
9373
9807
  return {
9374
- clarificationStillNeeded: true,
9375
- supervisorContext: lines.length > 0 ? lines.join(" ") : undefined
9808
+ append: (entry) => appendAuditEntry(directory, entry, appLog),
9809
+ query: (filter) => queryAudit(directory, filter)
9376
9810
  };
9377
9811
  }
9378
- var STOP_WORDS = new Set([
9379
- "with",
9380
- "that",
9381
- "this",
9382
- "from",
9383
- "into",
9384
- "when",
9385
- "then",
9386
- "will",
9387
- "have",
9388
- "been",
9389
- "does",
9390
- "should",
9391
- "would",
9392
- "could",
9393
- "after",
9394
- "before",
9395
- "about"
9396
- ]);
9397
- function classifyQuestionKind(questionLower) {
9398
- for (const { kind, patterns } of QUESTION_KIND_PATTERNS) {
9399
- if (patterns.some((p) => questionLower.includes(p)))
9400
- return kind;
9401
- }
9402
- return null;
9403
- }
9404
9812
 
9405
- // src/services/workflow-router.ts
9406
- function stage(name, command, requiresApproval, skippable, args) {
9407
- return { name, command, args, requiresApproval, skippable };
9813
+ // src/services/harness-policy.ts
9814
+ function allow4(reason, source, riskFlags = []) {
9815
+ return { verdict: "allow", reason, riskFlags, source };
9408
9816
  }
9409
- function scoreTaskForRouting(criteria) {
9410
- const simplicity = (criteria.taskType === "simple" ? 1 : 0) * 0.3;
9411
- const confidence = criteria.confidence * 0.2;
9412
- const lowRisk = !criteria.isSensitive && criteria.blastRadius < 3 ? 0.2 : 0;
9413
- const knownCodebase = criteria.codebaseFreshness === "fresh" ? 0.15 : 0;
9414
- const cheapComplexity = criteria.complexity === "cheap" ? 0.15 : 0;
9415
- const total = simplicity + confidence + lowRisk + knownCodebase + cheapComplexity;
9817
+ function deny3(reason, source, escalationMessage, riskFlags = []) {
9416
9818
  return {
9417
- simplicity,
9418
- confidence,
9419
- lowRisk,
9420
- knownCodebase,
9421
- cheapComplexity,
9422
- total
9819
+ verdict: "deny",
9820
+ reason,
9821
+ riskFlags,
9822
+ source,
9823
+ escalationMessage
9423
9824
  };
9424
9825
  }
9425
- function buildAdaptiveStageSequence(criteria) {
9426
- const scores = scoreTaskForRouting(criteria);
9427
- const totalScore = scores.total;
9428
- let workflowClass;
9429
- let stages;
9430
- let reason;
9431
- if (totalScore >= 0.75 && (criteria.taskType === "simple" || criteria.taskType === "docs")) {
9432
- workflowClass = "quick";
9433
- stages = [
9434
- stage("execute", "fd-execute", false, true),
9435
- stage("verify", "fd-verify", false, true)
9436
- ];
9437
- reason = `Quick workflow: score ${totalScore.toFixed(2)} >= 0.75 for ${criteria.taskType} task`;
9438
- } else if (criteria.taskType === "bugfix") {
9439
- workflowClass = "bugfix";
9440
- stages = [
9441
- stage("discuss", "fd-discuss", false, false),
9442
- stage("fix-bug", "fd-fix-bug", false, false),
9443
- stage("verify", "fd-verify", false, false)
9444
- ];
9445
- reason = "Bugfix workflow: task type is bugfix";
9446
- } else if (criteria.taskType === "docs" && totalScore < 0.75) {
9447
- workflowClass = "docs-only";
9448
- stages = [
9449
- stage("write-docs", "fd-write-docs", false, false),
9450
- stage("verify", "fd-verify", false, true)
9451
- ];
9452
- reason = `Docs-only workflow: score ${totalScore.toFixed(2)} < 0.75 for docs task`;
9453
- } else if (criteria.taskType === "ui-feature") {
9454
- workflowClass = "ui-heavy";
9455
- stages = [
9456
- stage("discuss", "fd-discuss", false, false),
9457
- stage("design", "fd-design", false, false, "--mode=draft"),
9458
- stage("plan", "fd-plan", true, false),
9459
- stage("execute", "fd-execute", false, false),
9460
- stage("verify", "fd-verify", false, false)
9461
- ];
9462
- reason = "UI-heavy workflow: task type indicates UI-heavy work";
9463
- } else if (criteria.blastRadius >= 5 || criteria.isSensitive) {
9464
- workflowClass = "verify-heavy";
9465
- stages = [
9466
- stage("plan", "fd-plan", true, false),
9467
- stage("execute", "fd-execute", false, false),
9468
- stage("verify", "fd-verify", false, false)
9469
- ];
9470
- reason = `Verify-heavy workflow: blastRadius=${criteria.blastRadius}, isSensitive=${criteria.isSensitive}`;
9471
- } else if (criteria.confidence < 0.6 || criteria.taskType === "ambiguous") {
9472
- workflowClass = "explore";
9473
- stages = [
9474
- stage("discuss", "fd-discuss", false, false),
9475
- stage("plan", "fd-plan", true, false),
9476
- stage("execute", "fd-execute", false, false),
9477
- stage("verify", "fd-verify", false, false)
9478
- ];
9479
- reason = `Explore workflow: confidence=${criteria.confidence}, taskType=${criteria.taskType}`;
9480
- } else {
9481
- workflowClass = "standard";
9482
- stages = [
9483
- stage("plan", "fd-plan", true, false),
9484
- stage("execute", "fd-execute", false, false),
9485
- stage("verify", "fd-verify", false, false)
9486
- ];
9487
- reason = `Standard workflow: score ${totalScore.toFixed(2)} with taskType ${criteria.taskType}`;
9488
- }
9826
+ function ask2(reason, source, approvalRequestId, riskFlags = []) {
9489
9827
  return {
9490
- workflowClass,
9491
- stages,
9492
- criteria,
9493
- scores,
9494
- reason
9828
+ verdict: "ask",
9829
+ reason,
9830
+ riskFlags,
9831
+ source,
9832
+ approvalRequestId
9495
9833
  };
9496
9834
  }
9497
-
9498
- // src/services/quick-router.ts
9499
- var BUG_SIGNALS = [
9500
- "fix",
9501
- "bug",
9502
- "broken",
9503
- "not working",
9504
- "doesn't work",
9505
- "does not work",
9506
- "error",
9507
- "crash",
9508
- "regression",
9509
- "debug",
9510
- "exception",
9511
- "failing",
9512
- "fails",
9513
- "incorrect",
9514
- "wrong output",
9515
- "infinite loop",
9516
- "null pointer",
9517
- "undefined",
9518
- "404",
9519
- "500",
9520
- "stack trace",
9521
- "traceback",
9522
- "root cause",
9523
- "why is"
9524
- ];
9525
- var UI_SIGNALS = [
9526
- "landing page",
9527
- "dashboard",
9528
- "admin panel",
9529
- "admin page",
9530
- "app screen",
9531
- "onboarding",
9532
- "onboard",
9533
- "wireframe",
9534
- "mockup",
9535
- "design system",
9536
- "component library",
9537
- "ui component",
9538
- "ux flow",
9539
- "user interface",
9540
- "web app",
9541
- "web application",
9542
- "website",
9543
- "frontend page",
9544
- "mobile screen",
9545
- "login page",
9546
- "signup page",
9547
- "settings page",
9548
- "profile page",
9549
- "modal",
9550
- "dialog",
9551
- "sidebar",
9552
- "navigation",
9553
- "navbar",
9554
- "header",
9555
- "footer",
9556
- "layout",
9557
- "responsive",
9558
- "accessibility",
9559
- "a11y",
9560
- "dark mode",
9561
- "theme"
9562
- ];
9563
- var DOCS_SIGNALS = [
9564
- "docs",
9565
- "documentation",
9566
- "readme",
9567
- "api docs",
9568
- "usage guide",
9569
- "write docs",
9570
- "document",
9571
- "document the",
9572
- "how to use",
9573
- "tutorial",
9574
- "changelog",
9575
- "contributing guide",
9576
- "docstring",
9577
- "jsdoc",
9578
- "tsdoc"
9579
- ];
9580
- var SIMPLE_SIGNALS = [
9581
- "rename",
9582
- "move file",
9583
- "quick",
9584
- "minor",
9585
- "small change",
9586
- "one-liner",
9587
- "typo",
9588
- "update constant",
9589
- "update config",
9590
- "bump version"
9591
- ];
9592
- var AMBIGUOUS_PATTERNS = [
9593
- /^(improve|make|update|change|add|remove|help|do|run|check|use)\s+\w+$/i
9594
- ];
9595
- function classifyTask(description) {
9596
- const lower = description.toLowerCase().trim();
9597
- if (!lower) {
9598
- return _ambiguous([], "What task do you want to run? Please describe what you need done.");
9835
+ function validationToDecision(result) {
9836
+ const riskFlags = result.violations.map((v) => v.rule);
9837
+ if (result.action === "block") {
9838
+ return deny3(result.message ?? "Agent contract violation", "agent-contract", result.message ?? "This tool call violates the agent's capability contract", riskFlags);
9599
9839
  }
9600
- const bugHits = BUG_SIGNALS.filter((s) => lower.includes(s));
9601
- const uiHits = UI_SIGNALS.filter((s) => lower.includes(s));
9602
- const docsHits = DOCS_SIGNALS.filter((s) => lower.includes(s));
9603
- const simpleHits = SIMPLE_SIGNALS.filter((s) => lower.includes(s));
9604
- const bugScore = Math.min(bugHits.length * 0.35, 1);
9605
- const uiScore = Math.min(uiHits.length * 0.3, 1);
9606
- const docsScore = Math.min(docsHits.length * 0.4, 1);
9607
- const simpleScore = Math.min(simpleHits.length * 0.45, 1);
9608
- if (bugScore >= 0.35 && bugScore >= uiScore && bugScore >= docsScore) {
9609
- return {
9610
- taskType: "bugfix",
9611
- confidence: Math.min(0.5 + bugScore * 0.5, 0.98),
9612
- signals: bugHits,
9613
- requiresDesign: false,
9614
- requiresTDD: true,
9615
- stageSequence: buildStageSequence("bugfix"),
9616
- clarificationNeeded: bugScore < 0.5,
9617
- clarificationPrompt: bugScore < 0.5 ? "Can you describe the specific bug? What is the expected vs actual behavior?" : undefined
9618
- };
9840
+ if (result.action === "escalate") {
9841
+ return ask2(result.message ?? "Agent contract requires escalation", "agent-contract", undefined, riskFlags);
9619
9842
  }
9620
- if (uiScore >= 0.3) {
9621
- return {
9622
- taskType: "ui-feature",
9623
- confidence: Math.min(0.5 + uiScore * 0.45, 0.95),
9624
- signals: uiHits,
9625
- requiresDesign: true,
9626
- requiresTDD: true,
9627
- stageSequence: buildStageSequence("ui-feature"),
9628
- clarificationNeeded: false
9629
- };
9843
+ if (result.action === "warn") {
9844
+ return allow4(result.message ?? "Agent contract advisory", "agent-contract", riskFlags);
9630
9845
  }
9631
- if (docsScore >= 0.4 && docsScore >= bugScore) {
9632
- return {
9633
- taskType: "docs",
9634
- confidence: Math.min(0.55 + docsScore * 0.4, 0.95),
9635
- signals: docsHits,
9636
- requiresDesign: false,
9637
- requiresTDD: false,
9638
- stageSequence: buildStageSequence("docs"),
9639
- clarificationNeeded: false
9640
- };
9846
+ return allow4("Agent contract passed", "agent-contract");
9847
+ }
9848
+ function buildAuditEntry(input, decision) {
9849
+ return {
9850
+ id: randomUUID4(),
9851
+ timestamp: new Date().toISOString(),
9852
+ run_id: input.runId,
9853
+ session_id: input.sessionID,
9854
+ agent: input.agent,
9855
+ tool: input.tool,
9856
+ command: input.command,
9857
+ verdict: decision.verdict,
9858
+ reason: decision.reason,
9859
+ risk_flags: decision.riskFlags,
9860
+ source: decision.source,
9861
+ approval_request_id: decision.approvalRequestId
9862
+ };
9863
+ }
9864
+ function createHarnessPolicy(directory, appLog) {
9865
+ const config = loadFlowDeckConfig(directory);
9866
+ const auditLog = createAuditLog(directory, appLog);
9867
+ const loopDetector = new LoopDetector({
9868
+ enabled: config.governance?.loopDetection?.enabled ?? true,
9869
+ maxRepeats: config.governance?.loopDetection?.maxRepeats ?? 2,
9870
+ similarityThreshold: config.governance?.loopDetection?.similarityThreshold ?? 0.9,
9871
+ historySize: config.governance?.loopDetection?.historySize ?? 20
9872
+ }, appLog);
9873
+ function appendAudit(input, decision) {
9874
+ try {
9875
+ auditLog.append(buildAuditEntry(input, decision));
9876
+ } catch (error) {
9877
+ if (appLog) {
9878
+ const message = error instanceof Error ? error.message : String(error);
9879
+ appLog(`[harness-policy] audit append failed: ${message}`);
9880
+ }
9881
+ }
9641
9882
  }
9642
- if (simpleScore >= 0.45) {
9643
- return {
9644
- taskType: "simple",
9645
- confidence: Math.min(0.55 + simpleScore * 0.35, 0.9),
9646
- signals: simpleHits,
9647
- requiresDesign: false,
9648
- requiresTDD: false,
9649
- stageSequence: buildStageSequence("simple"),
9650
- clarificationNeeded: false
9651
- };
9883
+ function checkRuntimeLimits(input) {
9884
+ try {
9885
+ const loop = loopDetector.checkBefore(input.tool, input.args, input.sessionID);
9886
+ if (loop.action === "block") {
9887
+ return deny3(loop.reason, "loop-detector", loop.escalationMessage, ["loop-detected"]);
9888
+ }
9889
+ if (loop.action === "warn") {
9890
+ return allow4(loop.message, "loop-detector", ["loop-warning"]);
9891
+ }
9892
+ if (isTraceStuck(input.directory, input.runId)) {
9893
+ 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"]);
9894
+ }
9895
+ return allow4("Runtime limits passed", "runtime-limits");
9896
+ } catch (error) {
9897
+ const message = error instanceof Error ? error.message : String(error);
9898
+ if (appLog)
9899
+ appLog(`[harness-policy] runtime limits error: ${message}`);
9900
+ return deny3(`Runtime limits check failed: ${message}`, "runtime-limits", `Runtime limits check failed: ${message}`, ["runtime-check-error"]);
9901
+ }
9652
9902
  }
9653
- const wordCount = lower.split(/\s+/).filter(Boolean).length;
9654
- if (wordCount >= 5) {
9655
- return {
9656
- taskType: "feature",
9657
- confidence: Math.min(0.5 + wordCount * 0.02, 0.85),
9658
- signals: [],
9659
- requiresDesign: false,
9660
- requiresTDD: true,
9661
- stageSequence: buildStageSequence("feature"),
9662
- clarificationNeeded: wordCount < 8,
9663
- clarificationPrompt: wordCount < 8 ? "Is this a new feature, a bug fix, or a documentation task? A bit more context will help route it correctly." : undefined
9664
- };
9903
+ function checkAgentContract(input) {
9904
+ try {
9905
+ const result = validateToolAccess(input.directory, input.agent, input.tool, { run_id: input.runId, session_id: input.sessionID });
9906
+ return validationToDecision(result);
9907
+ } catch (error) {
9908
+ const message = error instanceof Error ? error.message : String(error);
9909
+ if (appLog)
9910
+ appLog(`[harness-policy] agent contract error: ${message}`);
9911
+ return deny3(`Agent contract check failed: ${message}`, "agent-contract", `Agent contract check failed: ${message}`, ["agent-contract-error"]);
9912
+ }
9913
+ }
9914
+ function evaluatePolicyEngine(input) {
9915
+ try {
9916
+ const store = readStore2(input.directory);
9917
+ const filePath = extractTargetPath(input.args);
9918
+ const active = store.policies.filter((p) => p.active);
9919
+ for (const policy of active) {
9920
+ const trigger = policy.trigger.toLowerCase();
9921
+ if (!trigger.trim())
9922
+ continue;
9923
+ if (input.tool.toLowerCase().includes(trigger) || filePath.toLowerCase().includes(trigger)) {
9924
+ return deny3(`Active policy violation: ${policy.name}`, "policy-engine", policy.rule, ["policy-violation", policy.id]);
9925
+ }
9926
+ }
9927
+ return allow4("No active policy violations", "policy-engine");
9928
+ } catch (error) {
9929
+ const message = error instanceof Error ? error.message : String(error);
9930
+ if (appLog)
9931
+ appLog(`[harness-policy] policy engine error: ${message}`);
9932
+ return allow4(`Policy engine check failed: ${message}`, "policy-engine", [
9933
+ "policy-engine-error"
9934
+ ]);
9935
+ }
9665
9936
  }
9666
- const isAmbiguousPattern = AMBIGUOUS_PATTERNS.some((p) => p.test(lower));
9667
- if (isAmbiguousPattern || wordCount < 5) {
9668
- return _ambiguous([], "Can you describe the task in more detail? For example: is it a new feature, a bug fix, a UI change, or documentation?");
9937
+ function checkGovernance(input) {
9938
+ try {
9939
+ const riskFlags = [];
9940
+ if (config.governance?.toolGuard !== false) {
9941
+ const tg = evaluate(input);
9942
+ if (tg.verdict === "deny")
9943
+ return tg;
9944
+ riskFlags.push(...tg.riskFlags);
9945
+ }
9946
+ if (config.governance?.guardRails !== false) {
9947
+ const gr = evaluate2(input);
9948
+ if (gr.verdict === "deny")
9949
+ return gr;
9950
+ riskFlags.push(...gr.riskFlags);
9951
+ }
9952
+ if (config.governance?.approvals !== false) {
9953
+ const ap = evaluate3(input);
9954
+ if (ap.verdict !== "allow")
9955
+ return ap;
9956
+ }
9957
+ const pe = evaluatePolicyEngine(input);
9958
+ if (pe.verdict === "deny")
9959
+ return pe;
9960
+ riskFlags.push(...pe.riskFlags);
9961
+ return allow4("Governance checks passed", "governance", riskFlags);
9962
+ } catch (error) {
9963
+ const message = error instanceof Error ? error.message : String(error);
9964
+ if (appLog)
9965
+ appLog(`[harness-policy] governance error: ${message}`);
9966
+ return deny3(`Governance check failed: ${message}`, "governance", `Governance check failed: ${message}`, ["governance-error"]);
9967
+ }
9669
9968
  }
9670
- return {
9671
- taskType: "feature",
9672
- confidence: 0.6,
9673
- signals: [],
9674
- requiresDesign: false,
9675
- requiresTDD: true,
9676
- stageSequence: buildStageSequence("feature"),
9677
- clarificationNeeded: false
9678
- };
9679
- }
9680
- function _ambiguous(signals, prompt) {
9681
- return {
9682
- taskType: "ambiguous",
9683
- confidence: 0,
9684
- signals,
9685
- requiresDesign: false,
9686
- requiresTDD: false,
9687
- stageSequence: [],
9688
- clarificationNeeded: true,
9689
- clarificationPrompt: prompt
9690
- };
9691
- }
9692
- function buildStageSequence(taskType) {
9693
- switch (taskType) {
9694
- case "feature":
9695
- return [
9696
- stage2("discuss", "fd-discuss", false, false),
9697
- stage2("plan", "fd-plan", true, false),
9698
- stage2("execute", "fd-execute", false, false),
9699
- stage2("verify", "fd-verify", false, false)
9700
- ];
9701
- case "ui-feature":
9702
- return [
9703
- stage2("discuss", "fd-discuss", false, false),
9704
- stage2("design", "fd-design", false, false, "--mode=draft"),
9705
- stage2("plan", "fd-plan", true, false),
9706
- stage2("execute", "fd-execute", false, false),
9707
- stage2("verify", "fd-verify", false, false)
9708
- ];
9709
- case "bugfix":
9710
- return [
9711
- stage2("discuss", "fd-discuss", false, false),
9712
- stage2("fix-bug", "fd-fix-bug", false, false),
9713
- stage2("verify", "fd-verify", false, false)
9714
- ];
9715
- case "docs":
9716
- return [
9717
- stage2("discuss", "fd-discuss", false, false),
9718
- stage2("write-docs", "fd-write-docs", false, false),
9719
- stage2("verify", "fd-verify", false, true)
9720
- ];
9721
- case "simple":
9722
- return [
9723
- stage2("execute", "fd-execute", false, false),
9724
- stage2("verify", "fd-verify", false, true)
9725
- ];
9726
- case "ambiguous":
9727
- return [];
9728
- default:
9729
- return [];
9969
+ function checkSupervisor(input, _targetName) {
9970
+ if (config.governance?.supervisor?.enabled === false) {
9971
+ return allow4("Supervisor disabled", "supervisor");
9972
+ }
9973
+ try {
9974
+ return reviewToolCall(input.directory, input);
9975
+ } catch (error) {
9976
+ const message = error instanceof Error ? error.message : String(error);
9977
+ if (appLog)
9978
+ appLog(`[harness-policy] supervisor error: ${message}`);
9979
+ return deny3(`Supervisor review failed: ${message}`, "supervisor", `Supervisor review failed: ${message}`, ["supervisor-error"]);
9980
+ }
9730
9981
  }
9731
- }
9732
- function buildAdaptiveWorkflow(description, exploration) {
9733
- const base = exploration ? classifyTaskWithContext(description, exploration) : classifyTask(description);
9734
- const complexityResult = classifyTaskComplexity(description);
9735
- const criteria = {
9736
- taskType: base.taskType,
9737
- complexity: complexityResult.complexity,
9738
- confidence: base.confidence,
9739
- blastRadius: 0,
9740
- isSensitive: false,
9741
- codebaseFreshness: exploration ? "fresh" : "unknown",
9742
- requiresTests: base.requiresTDD
9743
- };
9744
- const route = buildAdaptiveStageSequence(criteria);
9745
- return {
9746
- ...base,
9747
- stageSequence: route.stages,
9748
- workflowClass: route.workflowClass,
9749
- scores: route.scores
9750
- };
9751
- }
9752
- function stage2(name, command, requiresApproval, skippable, args) {
9753
- return { name, command, args, requiresApproval, skippable };
9754
- }
9755
- function classifyTaskWithContext(description, exploration, sessionHistory = []) {
9756
- const base = classifyTask(description);
9757
- if (!base.clarificationNeeded) {
9758
- return base;
9982
+ function generateApprovalRequestId(input) {
9983
+ try {
9984
+ const req = requestApprovalForTool(input.directory, input.runId, input.sessionID, input.agent, input.tool, input.args);
9985
+ return req.id;
9986
+ } catch (error) {
9987
+ const message = error instanceof Error ? error.message : String(error);
9988
+ if (appLog)
9989
+ appLog(`[harness-policy] approval request failed: ${message}`);
9990
+ return;
9991
+ }
9759
9992
  }
9760
- const refinement = refineClassification(base.clarificationPrompt ?? "", exploration, sessionHistory);
9761
- if (!refinement.clarificationStillNeeded) {
9762
- const resolvedType = base.taskType === "ambiguous" ? "feature" : base.taskType;
9763
- return {
9764
- ...base,
9765
- taskType: resolvedType,
9766
- stageSequence: buildStageSequence(resolvedType),
9767
- clarificationNeeded: false,
9768
- clarificationPrompt: undefined,
9769
- confidence: Math.max(base.confidence, 0.55)
9770
- };
9993
+ function combineDecisions(decisions, input) {
9994
+ const denied = decisions.find((d) => d.verdict === "deny");
9995
+ if (denied)
9996
+ return denied;
9997
+ const asked = decisions.find((d) => d.verdict === "ask");
9998
+ if (asked) {
9999
+ return {
10000
+ ...asked,
10001
+ approvalRequestId: asked.approvalRequestId ?? generateApprovalRequestId(input)
10002
+ };
10003
+ }
10004
+ const riskFlags = decisions.flatMap((d) => d.riskFlags);
10005
+ const source = decisions.findLast((d) => d.source !== "harness.policy")?.source ?? "harness.policy";
10006
+ return allow4("All policy checks passed", source, riskFlags);
10007
+ }
10008
+ function evaluate4(input) {
10009
+ if (config.harness?.enabled === false) {
10010
+ const envDisabled = process.env.FLOWDECK_HARNESS_DISABLED === "1";
10011
+ if (!envDisabled) {
10012
+ if (appLog) {
10013
+ appLog("[harness-policy] SECURITY WARNING: harness.enabled=false without FLOWDECK_HARNESS_DISABLED=1. Enforcing minimal safety checks (loop detection, tool-guard dangerous patterns, orchestrator contract).");
10014
+ }
10015
+ const runtime2 = checkRuntimeLimits(input);
10016
+ if (runtime2.verdict === "deny") {
10017
+ appendAudit(input, runtime2);
10018
+ return runtime2;
10019
+ }
10020
+ if (config.governance?.toolGuard !== false) {
10021
+ const tg = evaluate(input);
10022
+ if (tg.verdict === "deny") {
10023
+ appendAudit(input, tg);
10024
+ return tg;
10025
+ }
10026
+ }
10027
+ const decision2 = allow4("Harness disabled — minimal safety checks passed", "harness.policy", ["harness-disabled-minimal"]);
10028
+ appendAudit(input, decision2);
10029
+ return decision2;
10030
+ }
10031
+ const decision = allow4("Harness disabled by config and env override", "harness.policy");
10032
+ appendAudit(input, decision);
10033
+ return decision;
10034
+ }
10035
+ const decisions = [];
10036
+ const runtime = checkRuntimeLimits(input);
10037
+ decisions.push(runtime);
10038
+ if (runtime.verdict === "deny") {
10039
+ appendAudit(input, runtime);
10040
+ return runtime;
10041
+ }
10042
+ const contract = checkAgentContract(input);
10043
+ decisions.push(contract);
10044
+ if (contract.verdict === "deny") {
10045
+ appendAudit(input, contract);
10046
+ return contract;
10047
+ }
10048
+ const governance = checkGovernance(input);
10049
+ decisions.push(governance);
10050
+ if (governance.verdict === "deny") {
10051
+ appendAudit(input, governance);
10052
+ return governance;
10053
+ }
10054
+ const supervisor = checkSupervisor(input, input.agent);
10055
+ decisions.push(supervisor);
10056
+ const final = combineDecisions(decisions, input);
10057
+ appendAudit(input, final);
10058
+ return final;
9771
10059
  }
9772
- const enrichedPrompt = refinement.supervisorContext ? `${base.clarificationPrompt ?? ""} (Context: ${refinement.supervisorContext})` : base.clarificationPrompt;
9773
10060
  return {
9774
- ...base,
9775
- clarificationPrompt: enrichedPrompt
10061
+ evaluate: evaluate4,
10062
+ checkAgentContract,
10063
+ checkRuntimeLimits,
10064
+ checkGovernance,
10065
+ checkSupervisor
9776
10066
  };
9777
10067
  }
9778
10068
 
10069
+ // src/services/harness-controller.ts
10070
+ import { randomUUID as randomUUID7 } from "crypto";
10071
+
9779
10072
  // src/services/run-trace.ts
9780
10073
  import { existsSync as existsSync26, readFileSync as readFileSync26, appendFileSync as appendFileSync7, writeFileSync as writeFileSync16, mkdirSync as mkdirSync16 } from "fs";
9781
10074
  import { join as join26 } from "path";
@@ -10468,26 +10761,79 @@ var DISABLED = process.env.FLOWDECK_ORCHESTRATOR_GUARD === "off";
10468
10761
  function normalizeToolName(name) {
10469
10762
  return name.toLowerCase().replace(/[-_]/g, "");
10470
10763
  }
10471
- function blockMessage(toolName) {
10764
+ var WRITE_TOOLS2 = new Set(["write", "writefile", "create", "createfile", "edit", "editfile", "patch", "applypatch", "strreplaceeditor", "strreplace"]);
10765
+ var SHELL_TOOLS = new Set(["bash", "runbash", "execute", "runcommand", "terminal", "shell"]);
10766
+ var BUILD_TOOLS = new Set(["npm", "pnpm", "yarn", "bun", "cargo", "go", "make", "cmake", "docker", "kubectl", "terraform", "pulumi"]);
10767
+ var SCRIPT_TOOLS = new Set(["python", "runpython", "js", "runjs"]);
10768
+ function agentCanExecute(contract) {
10769
+ const allowed = contract.allowedTools.map(normalizeToolName);
10770
+ return allowed.some((t) => WRITE_TOOLS2.has(t) || SHELL_TOOLS.has(t) || BUILD_TOOLS.has(t));
10771
+ }
10772
+ function matchesBlockedTool(contract, blockedTool) {
10773
+ const normalizedBlocked = normalizeToolName(blockedTool);
10774
+ const allowed = contract.allowedTools.map(normalizeToolName);
10775
+ if (allowed.includes(normalizedBlocked))
10776
+ return true;
10777
+ if (WRITE_TOOLS2.has(normalizedBlocked))
10778
+ return allowed.some((t) => WRITE_TOOLS2.has(t));
10779
+ if (SHELL_TOOLS.has(normalizedBlocked))
10780
+ return allowed.some((t) => SHELL_TOOLS.has(t));
10781
+ if (BUILD_TOOLS.has(normalizedBlocked))
10782
+ return allowed.some((t) => BUILD_TOOLS.has(t));
10783
+ if (SCRIPT_TOOLS.has(normalizedBlocked))
10784
+ return allowed.some((t) => SCRIPT_TOOLS.has(t));
10785
+ return false;
10786
+ }
10787
+ function fallbackRoutingSuggestion(blockedTool) {
10788
+ const normalized = normalizeToolName(blockedTool);
10789
+ if (WRITE_TOOLS2.has(normalized)) {
10790
+ return ` @default-executor — simple file edits and quick fixes
10791
+ @backend-coder — backend code changes
10792
+ @frontend-coder — frontend code changes
10793
+ @writer — documentation changes`;
10794
+ }
10795
+ if (SHELL_TOOLS.has(normalized)) {
10796
+ return ` @default-executor — ad-hoc shell commands
10797
+ @tester — test and verification commands
10798
+ @devops — CI/CD and infrastructure commands`;
10799
+ }
10800
+ if (BUILD_TOOLS.has(normalized)) {
10801
+ return ` @devops — build, deploy, and infrastructure
10802
+ @tester — test and verification commands
10803
+ @build-error-resolver — fix build/type errors`;
10804
+ }
10805
+ if (SCRIPT_TOOLS.has(normalized)) {
10806
+ return ` @default-executor — run scripts and small experiments
10807
+ @tester — test scripts and verification`;
10808
+ }
10809
+ return " @default-executor — general execution delegate";
10810
+ }
10811
+ function buildRoutingSuggestions(blockedTool) {
10812
+ const contracts = getAllContracts().filter((c) => c.agent !== "orchestrator" && agentCanExecute(c));
10813
+ const matches = contracts.filter((c) => matchesBlockedTool(c, blockedTool)).map((c) => ` @${c.agent} — ${c.role}`).slice(0, 5);
10814
+ if (matches.length > 0) {
10815
+ return matches.join(`
10816
+ `);
10817
+ }
10818
+ return fallbackRoutingSuggestion(blockedTool);
10819
+ }
10820
+ function buildBlockMessage(toolName, repeatCount) {
10472
10821
  const contract = getContract("orchestrator");
10473
10822
  const allowed = contract?.allowedTools ?? [];
10823
+ const suggestions = buildRoutingSuggestions(toolName);
10824
+ const loopWarning = repeatCount >= 2 ? `
10825
+ ⚠️ You have attempted this blocked tool ${repeatCount} times. Stop retrying and delegate to an agent instead.
10826
+ ` : "";
10474
10827
  return `[Orchestrator Guard] The orchestrator cannot use \`${toolName}\` directly.
10475
10828
 
10476
- ` + `The orchestrator is a coordinator, not an executor.
10477
-
10829
+ ` + `The orchestrator is a coordinator, not an executor.${loopWarning}
10478
10830
  ` + `Routing options:
10479
- ` + ` @default-executor — simple direct tasks (rename, typo fix, quick edit)
10480
- ` + ` @backend-coder — backend code writing and editing
10481
- ` + ` @frontend-coder — frontend code writing and editing
10482
- ` + ` @devops — CI/CD, deploy, and infrastructure changes
10483
- ` + ` @mapper — codebase mapping
10484
- ` + ` @researcher — focused research and file analysis
10485
- ` + ` @tester — tests, builds, and shell-heavy verification
10486
- ` + ` @writer — documentation writing
10831
+ ${suggestions}
10487
10832
 
10488
10833
  ` + `Allowed tools for orchestrator: ${allowed.join(", ")}.
10489
10834
 
10490
10835
  ` + `To route execution, mention the agent directly: @default-executor, @backend-coder, etc.
10836
+ ` + `If the task is unclear, ask the human ONE targeted clarifying question instead of retrying.
10491
10837
 
10492
10838
  ` + `To disable this guard: set FLOWDECK_ORCHESTRATOR_GUARD=off`;
10493
10839
  }
@@ -10495,6 +10841,7 @@ function blockMessage(toolName) {
10495
10841
  class OrchestratorGuard {
10496
10842
  primarySessionId = null;
10497
10843
  policy;
10844
+ blockedHistory = new Map;
10498
10845
  setPolicy(policy) {
10499
10846
  this.policy = policy;
10500
10847
  }
@@ -10505,6 +10852,7 @@ class OrchestratorGuard {
10505
10852
  if (deletedId && deletedId === this.primarySessionId) {
10506
10853
  this.primarySessionId = null;
10507
10854
  }
10855
+ this.blockedHistory.delete(deletedId ?? "");
10508
10856
  return;
10509
10857
  }
10510
10858
  if (eventType !== "session.created" && eventType !== "session.started")
@@ -10527,14 +10875,19 @@ class OrchestratorGuard {
10527
10875
  return;
10528
10876
  const contract = getContract("orchestrator");
10529
10877
  if (!contract) {
10530
- throw new Error(blockMessage(toolName));
10878
+ return buildBlockMessage(toolName, 1);
10531
10879
  }
10532
10880
  const normalizedTool = normalizeToolName(toolName);
10533
10881
  const allowed = contract.allowedTools.some((t) => normalizeToolName(t) === normalizedTool);
10534
10882
  if (allowed)
10535
10883
  return;
10536
- if (this.policy) {}
10537
- throw new Error(blockMessage(toolName));
10884
+ const sessionHistory = this.blockedHistory.get(sessionId) ?? new Map;
10885
+ const record = sessionHistory.get(normalizedTool) ?? { count: 0, lastTool: toolName };
10886
+ record.count += 1;
10887
+ record.lastTool = toolName;
10888
+ sessionHistory.set(normalizedTool, record);
10889
+ this.blockedHistory.set(sessionId, sessionHistory);
10890
+ return buildBlockMessage(toolName, record.count);
10538
10891
  }
10539
10892
  _isBlockedForTest(name) {
10540
10893
  const contract = getContract("orchestrator");
@@ -10549,6 +10902,9 @@ class OrchestratorGuard {
10549
10902
  _setPrimarySessionIdForTest(id) {
10550
10903
  this.primarySessionId = id;
10551
10904
  }
10905
+ _getRepeatCountForTest(sessionId, toolName) {
10906
+ return this.blockedHistory.get(sessionId)?.get(normalizeToolName(toolName))?.count ?? 0;
10907
+ }
10552
10908
  }
10553
10909
  function extractSessionId(event) {
10554
10910
  const props = event.properties;
@@ -11022,6 +11378,7 @@ function createHarnessController(config) {
11022
11378
  const maxToolCalls = delegationConfig?.maxToolCalls ?? 200;
11023
11379
  const sessionDepthMap = new Map;
11024
11380
  const blockedSessions = new Set;
11381
+ const pendingDepthBlockNotifications = new Map;
11025
11382
  const state = {
11026
11383
  loopDetector,
11027
11384
  eventLog,
@@ -11059,7 +11416,8 @@ function createHarnessController(config) {
11059
11416
  projectRoot: directory,
11060
11417
  command,
11061
11418
  description,
11062
- currentStage
11419
+ currentStage,
11420
+ workflowDecision: route
11063
11421
  });
11064
11422
  const budget = assembledContext.tokenBudget;
11065
11423
  log(`[context-ingress] run=${sessionID} trivial=${assembledContext.isTrivialChat} ` + `tokens=${budget.usedTokens}/${budget.limitTokens} (${budget.component})`);
@@ -11162,16 +11520,14 @@ function createHarnessController(config) {
11162
11520
  });
11163
11521
  state.lastActiveSessionID = req.sessionID;
11164
11522
  const agent = req.agent ?? "orchestrator";
11165
- try {
11166
- orchestratorGuard.check(req.sessionID, req.tool);
11167
- } catch (error) {
11168
- const message = error instanceof Error ? error.message : String(error);
11523
+ const guardMessage = orchestratorGuard.check(req.sessionID, req.tool);
11524
+ if (guardMessage) {
11169
11525
  return {
11170
11526
  verdict: "deny",
11171
- reason: message,
11527
+ reason: guardMessage,
11172
11528
  riskFlags: ["orchestrator-contract"],
11173
11529
  source: "orchestrator-guard",
11174
- escalationMessage: message
11530
+ escalationMessage: guardMessage
11175
11531
  };
11176
11532
  }
11177
11533
  executionSubstrate.start({
@@ -11236,6 +11592,9 @@ function createHarnessController(config) {
11236
11592
  if (childDepth > maxDepth) {
11237
11593
  log(`[harness] delegation depth ${childDepth} exceeds maxDepth ${maxDepth} — session ${sessionID} will be policy-blocked`);
11238
11594
  blockedSessions.add(sessionID);
11595
+ if (parentSessionID) {
11596
+ pendingDepthBlockNotifications.set(sessionID, parentSessionID);
11597
+ }
11239
11598
  }
11240
11599
  } else {
11241
11600
  sessionDepthMap.set(sessionID, 0);
@@ -11244,6 +11603,7 @@ function createHarnessController(config) {
11244
11603
  if ((type === "session.idle" || type === "session.error") && sessionID) {
11245
11604
  sessionDepthMap.delete(sessionID);
11246
11605
  blockedSessions.delete(sessionID);
11606
+ pendingDepthBlockNotifications.delete(sessionID);
11247
11607
  }
11248
11608
  orchestratorGuard.onEvent({ type, properties });
11249
11609
  if (type === "session.created" || type === "session.started") {
@@ -11288,6 +11648,14 @@ function createHarnessController(config) {
11288
11648
  },
11289
11649
  getContextMonitor() {
11290
11650
  return contextMonitor.get();
11651
+ },
11652
+ getPendingDepthBlockNotifications() {
11653
+ const notifications = [];
11654
+ for (const [sessionID, parentSessionID] of pendingDepthBlockNotifications) {
11655
+ notifications.push({ sessionID, parentSessionID });
11656
+ }
11657
+ pendingDepthBlockNotifications.clear();
11658
+ return notifications;
11291
11659
  }
11292
11660
  };
11293
11661
  }
@@ -11503,6 +11871,9 @@ var plugin = async (input, _options) => {
11503
11871
  type: event.type,
11504
11872
  properties: event.properties
11505
11873
  });
11874
+ for (const { sessionID: blockedSessionID, parentSessionID } of harness.getPendingDepthBlockNotifications()) {
11875
+ notify("FlowDeck: Delegation depth limit reached", `Session ${blockedSessionID} (parent ${parentSessionID}) exceeded max delegation depth. Stop further delegation from this subagent.`, "critical");
11876
+ }
11506
11877
  const type = event?.type ?? "";
11507
11878
  if (type === "command.executed") {
11508
11879
  const commandName = String(event?.properties?.name ?? "");
@@ -11533,6 +11904,11 @@ var plugin = async (input, _options) => {
11533
11904
  },
11534
11905
  "tool.execute.before": async (toolInput, toolOutput) => {
11535
11906
  const sessionID = toolInput.sessionID ?? "";
11907
+ harness.ensureRunContext({
11908
+ sessionID,
11909
+ description: `${toolInput.tool ?? toolInput.name ?? "unknown"} ${JSON.stringify(toolOutput?.args ?? toolInput?.args ?? {})} auto-invoked`,
11910
+ agent: toolInput.agent
11911
+ });
11536
11912
  const budgetCheck = harness.checkToolCallBudget(sessionID);
11537
11913
  if (!budgetCheck.allow) {
11538
11914
  throw new Error(budgetCheck.reason ?? `Tool blocked: ${budgetCheck.reason}`);