@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/agents/orchestrator.d.ts.map +1 -1
- package/dist/hooks/orchestrator-guard-hook.d.ts +14 -3
- package/dist/hooks/orchestrator-guard-hook.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2454 -2078
- package/dist/services/agent-contract-registry.d.ts.map +1 -1
- package/dist/services/context-ingress.d.ts +3 -0
- package/dist/services/context-ingress.d.ts.map +1 -1
- package/dist/services/harness-controller.d.ts +4 -0
- package/dist/services/harness-controller.d.ts.map +1 -1
- package/dist/services/harness-types.d.ts +3 -0
- package/dist/services/harness-types.d.ts.map +1 -1
- package/package.json +1 -1
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. **
|
|
1210
|
-
2. **
|
|
1211
|
-
3. **
|
|
1212
|
-
4. **
|
|
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
|
-
##
|
|
1217
|
+
## The Orchestrator Loop
|
|
1218
1218
|
|
|
1219
|
-
For EVERY
|
|
1219
|
+
For EVERY turn, follow this exact sequence:
|
|
1220
1220
|
|
|
1221
|
-
###
|
|
1222
|
-
- Read STATE.md if it exists
|
|
1223
|
-
- Identify current phase and
|
|
1224
|
-
-
|
|
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
|
-
###
|
|
1227
|
-
|
|
1228
|
-
-
|
|
1229
|
-
-
|
|
1230
|
-
-
|
|
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
|
-
###
|
|
1235
|
-
Select
|
|
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
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
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
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
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/
|
|
6346
|
-
|
|
6347
|
-
|
|
6348
|
-
|
|
6349
|
-
|
|
6350
|
-
|
|
6351
|
-
|
|
6352
|
-
|
|
6353
|
-
|
|
6354
|
-
|
|
6355
|
-
|
|
6356
|
-
|
|
6357
|
-
|
|
6358
|
-
|
|
6359
|
-
|
|
6360
|
-
|
|
6361
|
-
|
|
6362
|
-
|
|
6363
|
-
|
|
6364
|
-
|
|
6365
|
-
|
|
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
|
-
|
|
6368
|
-
|
|
6369
|
-
|
|
6370
|
-
|
|
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
|
-
|
|
6374
|
-
|
|
6375
|
-
|
|
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
|
-
|
|
6379
|
-
|
|
6380
|
-
|
|
6381
|
-
|
|
6382
|
-
|
|
6383
|
-
|
|
6384
|
-
|
|
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
|
-
|
|
6393
|
-
|
|
6455
|
+
const suppress = shouldSuppressQuestion(clarificationPrompt, result, sessionHistory);
|
|
6456
|
+
if (suppress.suppress) {
|
|
6457
|
+
return {
|
|
6458
|
+
clarificationStillNeeded: false,
|
|
6459
|
+
resolvedReason: suppress.reason
|
|
6460
|
+
};
|
|
6394
6461
|
}
|
|
6395
|
-
|
|
6396
|
-
|
|
6397
|
-
|
|
6398
|
-
|
|
6399
|
-
|
|
6400
|
-
|
|
6401
|
-
|
|
6402
|
-
|
|
6403
|
-
|
|
6404
|
-
|
|
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
|
-
|
|
6408
|
-
|
|
6409
|
-
|
|
6410
|
-
|
|
6411
|
-
|
|
6412
|
-
|
|
6413
|
-
|
|
6414
|
-
|
|
6415
|
-
|
|
6416
|
-
|
|
6417
|
-
|
|
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
|
-
|
|
6420
|
-
|
|
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
|
-
|
|
6423
|
-
|
|
6424
|
-
|
|
6425
|
-
|
|
6426
|
-
|
|
6427
|
-
|
|
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
|
|
6433
|
-
|
|
6434
|
-
|
|
6435
|
-
|
|
6436
|
-
|
|
6437
|
-
|
|
6438
|
-
|
|
6439
|
-
|
|
6440
|
-
|
|
6441
|
-
|
|
6442
|
-
|
|
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
|
|
7256
|
+
const stage3 = String(ctx.planningState.phase ?? "discuss");
|
|
6720
7257
|
if (ctx.isTrivialChat) {
|
|
6721
|
-
return `Greeting. Phase: ${
|
|
7258
|
+
return `Greeting. Phase: ${stage3}.`;
|
|
6722
7259
|
}
|
|
6723
7260
|
const languages = getCachedLanguages(ctx.directory);
|
|
6724
|
-
const cacheKey = sharedPromptCache.getKey(ctx.description,
|
|
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: ${
|
|
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
|
|
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
|
|
7476
|
+
const stage3 = currentStage ?? this.inferStageFromRoute(route);
|
|
6922
7477
|
const languages = getCachedLanguages(projectRoot);
|
|
6923
|
-
const cacheKey = `${projectRoot}\x00${
|
|
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
|
|
7293
|
-
allowedTaskTypes: ["orchestration", "coordination", "
|
|
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: "
|
|
7594
|
-
role: "
|
|
7595
|
-
allowedTaskTypes: ["
|
|
7596
|
-
requiredInputs: ["
|
|
7597
|
-
expectedOutputFields: ["
|
|
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
|
-
"
|
|
7601
|
-
"
|
|
8392
|
+
"optimize without measurements",
|
|
8393
|
+
"refactor unrelated code",
|
|
8394
|
+
"skip before/after verification"
|
|
7602
8395
|
],
|
|
7603
|
-
escalationConditions: ["
|
|
7604
|
-
stopConditions: ["
|
|
7605
|
-
successCriteria: [
|
|
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: "
|
|
7609
|
-
role: "
|
|
7610
|
-
allowedTaskTypes: ["
|
|
7611
|
-
requiredInputs: ["target
|
|
7612
|
-
expectedOutputFields: ["
|
|
7613
|
-
allowedTools: ["read", "
|
|
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
|
-
"
|
|
7616
|
-
"
|
|
7617
|
-
"
|
|
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
|
-
|
|
7628
|
-
|
|
7629
|
-
"
|
|
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
|
-
|
|
8434
|
+
escalationConditions: ["no valuable patterns found"],
|
|
8435
|
+
stopConditions: ["skills written or no new skills identified"],
|
|
7632
8436
|
successCriteria: [
|
|
7633
|
-
"
|
|
7634
|
-
"
|
|
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
|
-
|
|
7951
|
-
|
|
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 {
|
|
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
|
|
7956
|
-
|
|
7957
|
-
|
|
7958
|
-
|
|
7959
|
-
|
|
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
|
-
|
|
7962
|
-
|
|
7963
|
-
|
|
7964
|
-
|
|
7965
|
-
|
|
7966
|
-
|
|
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
|
-
|
|
7976
|
-
|
|
7977
|
-
|
|
7978
|
-
|
|
7979
|
-
|
|
7980
|
-
|
|
7981
|
-
|
|
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
|
|
8022
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8116
|
-
|
|
8117
|
-
|
|
8118
|
-
|
|
8119
|
-
|
|
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
|
|
8142
|
-
|
|
8143
|
-
|
|
8144
|
-
|
|
8145
|
-
|
|
8146
|
-
|
|
8147
|
-
|
|
8148
|
-
|
|
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
|
-
|
|
8151
|
-
|
|
8152
|
-
|
|
8153
|
-
|
|
8154
|
-
|
|
8155
|
-
|
|
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
|
-
|
|
8163
|
-
|
|
8164
|
-
|
|
8165
|
-
|
|
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 (
|
|
8172
|
-
|
|
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 (
|
|
8175
|
-
|
|
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
|
-
|
|
8178
|
-
|
|
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
|
|
8683
|
+
return { passed, reasons, riskFlags, missingRequirements, requiredChanges };
|
|
8181
8684
|
}
|
|
8182
|
-
function
|
|
8183
|
-
const
|
|
8184
|
-
|
|
8185
|
-
|
|
8186
|
-
|
|
8187
|
-
|
|
8188
|
-
|
|
8189
|
-
|
|
8190
|
-
return
|
|
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 (
|
|
8193
|
-
const
|
|
8194
|
-
|
|
8195
|
-
|
|
8196
|
-
|
|
8197
|
-
|
|
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
|
-
|
|
8204
|
-
|
|
8205
|
-
|
|
8206
|
-
|
|
8207
|
-
|
|
8208
|
-
|
|
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
|
-
|
|
8232
|
-
|
|
8233
|
-
|
|
8234
|
-
|
|
8235
|
-
|
|
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
|
-
|
|
8248
|
-
|
|
8721
|
+
const passed = missingRequirements.length === 0 && riskFlags.length === 0;
|
|
8722
|
+
if (passed) {
|
|
8723
|
+
reasons.push(`Agent "${agentName}" passed all policy checks`);
|
|
8249
8724
|
}
|
|
8250
|
-
|
|
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
|
|
8259
|
-
|
|
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
|
|
8262
|
-
|
|
8263
|
-
|
|
8264
|
-
|
|
8265
|
-
|
|
8266
|
-
|
|
8267
|
-
|
|
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
|
|
8271
|
-
const
|
|
8272
|
-
|
|
8273
|
-
|
|
8274
|
-
|
|
8275
|
-
|
|
8276
|
-
|
|
8277
|
-
|
|
8278
|
-
|
|
8279
|
-
|
|
8280
|
-
|
|
8281
|
-
|
|
8282
|
-
|
|
8283
|
-
|
|
8284
|
-
|
|
8285
|
-
|
|
8286
|
-
return
|
|
8287
|
-
|
|
8288
|
-
|
|
8289
|
-
|
|
8290
|
-
|
|
8291
|
-
|
|
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
|
|
8298
|
-
import { existsSync as
|
|
8299
|
-
import { join as
|
|
8300
|
-
|
|
8301
|
-
|
|
8302
|
-
var
|
|
8303
|
-
|
|
8304
|
-
|
|
8305
|
-
|
|
8306
|
-
|
|
8307
|
-
|
|
8308
|
-
|
|
8309
|
-
|
|
8310
|
-
|
|
8311
|
-
|
|
8312
|
-
|
|
8313
|
-
|
|
8314
|
-
|
|
8315
|
-
|
|
8316
|
-
|
|
8317
|
-
|
|
8318
|
-
|
|
8319
|
-
|
|
8320
|
-
|
|
8321
|
-
|
|
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
|
-
|
|
8356
|
-
|
|
8357
|
-
|
|
8358
|
-
|
|
8359
|
-
|
|
8360
|
-
|
|
8361
|
-
|
|
8362
|
-
|
|
8363
|
-
|
|
8364
|
-
|
|
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
|
-
|
|
8368
|
-
|
|
8369
|
-
|
|
8370
|
-
|
|
8371
|
-
|
|
8372
|
-
|
|
8373
|
-
|
|
8374
|
-
|
|
8375
|
-
|
|
8376
|
-
|
|
8377
|
-
|
|
8378
|
-
|
|
8379
|
-
|
|
8380
|
-
|
|
8381
|
-
|
|
8382
|
-
if
|
|
8383
|
-
|
|
8384
|
-
|
|
8385
|
-
|
|
8386
|
-
|
|
8387
|
-
|
|
8388
|
-
}
|
|
8389
|
-
|
|
8390
|
-
|
|
8391
|
-
|
|
8392
|
-
|
|
8393
|
-
|
|
8394
|
-
|
|
8395
|
-
|
|
8396
|
-
|
|
8397
|
-
|
|
8398
|
-
|
|
8399
|
-
|
|
8400
|
-
|
|
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
|
-
|
|
8416
|
-
|
|
8417
|
-
|
|
8418
|
-
|
|
8419
|
-
|
|
8420
|
-
|
|
8421
|
-
|
|
8422
|
-
|
|
8423
|
-
|
|
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
|
|
8967
|
+
return false;
|
|
8428
8968
|
}
|
|
8429
|
-
function
|
|
8430
|
-
const
|
|
8431
|
-
|
|
8432
|
-
|
|
8433
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
8985
|
+
return null;
|
|
8485
8986
|
}
|
|
8486
|
-
function
|
|
8487
|
-
|
|
8488
|
-
|
|
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
|
-
|
|
8491
|
-
|
|
8492
|
-
|
|
8493
|
-
|
|
8494
|
-
|
|
8495
|
-
|
|
8496
|
-
|
|
8497
|
-
|
|
8498
|
-
|
|
8499
|
-
|
|
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
|
|
8547
|
-
const
|
|
8548
|
-
if (!
|
|
8549
|
-
|
|
8550
|
-
|
|
8551
|
-
|
|
8552
|
-
|
|
8553
|
-
|
|
8554
|
-
|
|
8555
|
-
|
|
8556
|
-
|
|
8557
|
-
|
|
8558
|
-
|
|
8559
|
-
|
|
8560
|
-
|
|
8561
|
-
|
|
8562
|
-
|
|
8563
|
-
|
|
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
|
|
8595
|
-
|
|
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
|
-
|
|
8599
|
-
|
|
8600
|
-
|
|
8601
|
-
|
|
8602
|
-
|
|
8603
|
-
|
|
8604
|
-
|
|
8605
|
-
|
|
8606
|
-
|
|
8607
|
-
|
|
8608
|
-
|
|
8609
|
-
|
|
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
|
|
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: "
|
|
9068
|
+
verdict: "deny",
|
|
8614
9069
|
reason,
|
|
8615
9070
|
riskFlags,
|
|
8616
|
-
source: "
|
|
9071
|
+
source: "tool-guard",
|
|
9072
|
+
escalationMessage
|
|
8617
9073
|
};
|
|
8618
9074
|
}
|
|
8619
|
-
function
|
|
9075
|
+
function evaluate(input) {
|
|
8620
9076
|
const { directory, tool: tool13, args } = input;
|
|
8621
|
-
if (
|
|
8622
|
-
return
|
|
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
|
-
|
|
8629
|
-
|
|
9080
|
+
const blocked = isBlocked(tool13, args);
|
|
9081
|
+
if (blocked) {
|
|
9082
|
+
return deny(blocked, blocked, ["dangerous-pattern"]);
|
|
8630
9083
|
}
|
|
8631
|
-
|
|
8632
|
-
|
|
8633
|
-
|
|
8634
|
-
|
|
8635
|
-
|
|
8636
|
-
|
|
8637
|
-
|
|
8638
|
-
|
|
8639
|
-
|
|
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
|
|
9099
|
+
return allow("Tool guard passed");
|
|
8642
9100
|
}
|
|
8643
9101
|
|
|
8644
|
-
// src/
|
|
8645
|
-
import { existsSync as
|
|
8646
|
-
import { join as
|
|
8647
|
-
|
|
8648
|
-
|
|
8649
|
-
|
|
8650
|
-
|
|
8651
|
-
|
|
8652
|
-
|
|
8653
|
-
|
|
8654
|
-
|
|
8655
|
-
|
|
8656
|
-
|
|
8657
|
-
|
|
8658
|
-
|
|
8659
|
-
|
|
8660
|
-
|
|
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
|
-
|
|
8668
|
-
|
|
8669
|
-
|
|
8670
|
-
|
|
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
|
-
|
|
8732
|
-
|
|
8733
|
-
|
|
8734
|
-
|
|
8735
|
-
|
|
8736
|
-
|
|
8737
|
-
|
|
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
|
|
8740
|
-
|
|
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
|
|
8743
|
-
|
|
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
|
-
|
|
8747
|
-
|
|
8748
|
-
|
|
8749
|
-
|
|
8750
|
-
|
|
8751
|
-
return
|
|
8752
|
-
|
|
8753
|
-
|
|
8754
|
-
|
|
8755
|
-
|
|
8756
|
-
|
|
8757
|
-
|
|
8758
|
-
|
|
8759
|
-
return
|
|
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
|
|
8763
|
-
|
|
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
|
|
8766
|
-
|
|
8767
|
-
|
|
8768
|
-
|
|
8769
|
-
|
|
8770
|
-
|
|
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
|
|
8773
|
-
|
|
8774
|
-
|
|
8775
|
-
return [];
|
|
9274
|
+
function getPlanConfirmed(statePath3) {
|
|
9275
|
+
if (!existsSync21(statePath3))
|
|
9276
|
+
return false;
|
|
8776
9277
|
try {
|
|
8777
|
-
const
|
|
8778
|
-
|
|
8779
|
-
return
|
|
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
|
|
8785
|
-
|
|
8786
|
-
|
|
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
|
|
9289
|
+
return "Plan not confirmed. Run /fd-plan and confirm to enable execution.";
|
|
8807
9290
|
}
|
|
8808
|
-
function
|
|
8809
|
-
|
|
8810
|
-
|
|
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
|
-
|
|
8832
|
-
|
|
8833
|
-
|
|
8834
|
-
|
|
8835
|
-
|
|
8836
|
-
|
|
8837
|
-
|
|
8838
|
-
|
|
8839
|
-
|
|
8840
|
-
|
|
8841
|
-
|
|
8842
|
-
|
|
8843
|
-
|
|
8844
|
-
|
|
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
|
|
8852
|
-
const
|
|
8853
|
-
|
|
8854
|
-
|
|
8855
|
-
|
|
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
|
|
8879
|
-
const
|
|
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
|
-
|
|
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
|
|
8903
|
-
const
|
|
8904
|
-
|
|
8905
|
-
|
|
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
|
|
8919
|
-
|
|
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/
|
|
8923
|
-
|
|
8924
|
-
|
|
8925
|
-
|
|
8926
|
-
|
|
8927
|
-
|
|
8928
|
-
|
|
8929
|
-
|
|
8930
|
-
|
|
8931
|
-
|
|
8932
|
-
|
|
8933
|
-
|
|
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
|
|
8939
|
-
|
|
8940
|
-
|
|
8941
|
-
|
|
8942
|
-
|
|
9416
|
+
function ask(reason, riskFlags = []) {
|
|
9417
|
+
return {
|
|
9418
|
+
verdict: "ask",
|
|
9419
|
+
reason,
|
|
9420
|
+
riskFlags,
|
|
9421
|
+
source: "approval-manager"
|
|
9422
|
+
};
|
|
8943
9423
|
}
|
|
8944
|
-
function
|
|
8945
|
-
|
|
8946
|
-
|
|
8947
|
-
|
|
8948
|
-
|
|
8949
|
-
|
|
8950
|
-
|
|
8951
|
-
|
|
8952
|
-
|
|
8953
|
-
|
|
8954
|
-
|
|
8955
|
-
|
|
8956
|
-
|
|
8957
|
-
|
|
8958
|
-
|
|
8959
|
-
|
|
8960
|
-
|
|
8961
|
-
|
|
8962
|
-
|
|
8963
|
-
|
|
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
|
-
|
|
8968
|
-
|
|
8969
|
-
|
|
8970
|
-
|
|
8971
|
-
|
|
8972
|
-
|
|
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
|
|
8975
|
-
const
|
|
8976
|
-
if (!
|
|
9461
|
+
function loadAllSpans(dir) {
|
|
9462
|
+
const p = agentSpansPath(dir);
|
|
9463
|
+
if (!existsSync23(p))
|
|
8977
9464
|
return [];
|
|
8978
9465
|
try {
|
|
8979
|
-
|
|
8980
|
-
`).filter((
|
|
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
|
|
9002
|
-
|
|
9003
|
-
|
|
9004
|
-
|
|
9005
|
-
|
|
9006
|
-
|
|
9007
|
-
|
|
9008
|
-
|
|
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
|
|
9013
|
-
|
|
9014
|
-
|
|
9015
|
-
|
|
9016
|
-
|
|
9017
|
-
|
|
9018
|
-
|
|
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
|
|
9022
|
-
|
|
9023
|
-
|
|
9024
|
-
|
|
9025
|
-
|
|
9026
|
-
|
|
9027
|
-
|
|
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
|
|
9031
|
-
const
|
|
9032
|
-
|
|
9033
|
-
|
|
9034
|
-
|
|
9035
|
-
if (
|
|
9036
|
-
|
|
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
|
|
9044
|
-
|
|
9045
|
-
|
|
9046
|
-
|
|
9047
|
-
|
|
9048
|
-
|
|
9049
|
-
|
|
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
|
|
9060
|
-
|
|
9061
|
-
|
|
9062
|
-
|
|
9063
|
-
|
|
9064
|
-
|
|
9065
|
-
|
|
9066
|
-
|
|
9067
|
-
|
|
9068
|
-
|
|
9069
|
-
|
|
9070
|
-
|
|
9071
|
-
|
|
9072
|
-
|
|
9073
|
-
|
|
9074
|
-
|
|
9075
|
-
|
|
9076
|
-
|
|
9077
|
-
|
|
9078
|
-
|
|
9079
|
-
|
|
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
|
-
|
|
9165
|
-
|
|
9166
|
-
|
|
9167
|
-
|
|
9168
|
-
|
|
9169
|
-
|
|
9170
|
-
|
|
9171
|
-
|
|
9172
|
-
|
|
9173
|
-
|
|
9174
|
-
|
|
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
|
-
|
|
9178
|
-
|
|
9179
|
-
|
|
9180
|
-
|
|
9181
|
-
|
|
9182
|
-
|
|
9183
|
-
|
|
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
|
-
|
|
9189
|
-
|
|
9190
|
-
|
|
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
|
-
|
|
9196
|
-
|
|
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
|
-
|
|
9204
|
-
|
|
9205
|
-
|
|
9206
|
-
|
|
9207
|
-
|
|
9208
|
-
|
|
9209
|
-
|
|
9210
|
-
|
|
9211
|
-
|
|
9212
|
-
|
|
9213
|
-
|
|
9214
|
-
|
|
9215
|
-
|
|
9216
|
-
|
|
9217
|
-
|
|
9218
|
-
|
|
9219
|
-
|
|
9220
|
-
|
|
9221
|
-
|
|
9222
|
-
|
|
9223
|
-
|
|
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
|
-
|
|
9244
|
-
|
|
9245
|
-
|
|
9246
|
-
|
|
9247
|
-
|
|
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
|
-
|
|
9265
|
-
|
|
9266
|
-
|
|
9267
|
-
|
|
9268
|
-
|
|
9269
|
-
|
|
9270
|
-
|
|
9271
|
-
|
|
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
|
-
|
|
9323
|
-
|
|
9324
|
-
|
|
9325
|
-
|
|
9326
|
-
|
|
9327
|
-
|
|
9328
|
-
|
|
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
|
-
|
|
9332
|
-
|
|
9333
|
-
|
|
9334
|
-
|
|
9335
|
-
|
|
9336
|
-
|
|
9337
|
-
|
|
9338
|
-
|
|
9339
|
-
|
|
9340
|
-
|
|
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
|
|
9705
|
+
return null;
|
|
9344
9706
|
}
|
|
9345
|
-
function
|
|
9346
|
-
const
|
|
9347
|
-
|
|
9348
|
-
|
|
9349
|
-
|
|
9350
|
-
|
|
9351
|
-
|
|
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
|
-
|
|
9355
|
-
|
|
9356
|
-
|
|
9357
|
-
|
|
9358
|
-
|
|
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
|
-
|
|
9362
|
-
|
|
9363
|
-
|
|
9364
|
-
|
|
9365
|
-
|
|
9366
|
-
|
|
9367
|
-
|
|
9368
|
-
|
|
9369
|
-
|
|
9370
|
-
|
|
9371
|
-
|
|
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
|
-
|
|
9375
|
-
|
|
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/
|
|
9406
|
-
function
|
|
9407
|
-
return {
|
|
9813
|
+
// src/services/harness-policy.ts
|
|
9814
|
+
function allow4(reason, source, riskFlags = []) {
|
|
9815
|
+
return { verdict: "allow", reason, riskFlags, source };
|
|
9408
9816
|
}
|
|
9409
|
-
function
|
|
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
|
-
|
|
9418
|
-
|
|
9419
|
-
|
|
9420
|
-
|
|
9421
|
-
|
|
9422
|
-
total
|
|
9819
|
+
verdict: "deny",
|
|
9820
|
+
reason,
|
|
9821
|
+
riskFlags,
|
|
9822
|
+
source,
|
|
9823
|
+
escalationMessage
|
|
9423
9824
|
};
|
|
9424
9825
|
}
|
|
9425
|
-
function
|
|
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
|
-
|
|
9491
|
-
|
|
9492
|
-
|
|
9493
|
-
|
|
9494
|
-
|
|
9828
|
+
verdict: "ask",
|
|
9829
|
+
reason,
|
|
9830
|
+
riskFlags,
|
|
9831
|
+
source,
|
|
9832
|
+
approvalRequestId
|
|
9495
9833
|
};
|
|
9496
9834
|
}
|
|
9497
|
-
|
|
9498
|
-
|
|
9499
|
-
|
|
9500
|
-
|
|
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
|
-
|
|
9601
|
-
|
|
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 (
|
|
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
|
-
|
|
9632
|
-
|
|
9633
|
-
|
|
9634
|
-
|
|
9635
|
-
|
|
9636
|
-
|
|
9637
|
-
|
|
9638
|
-
|
|
9639
|
-
|
|
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
|
-
|
|
9643
|
-
|
|
9644
|
-
|
|
9645
|
-
|
|
9646
|
-
|
|
9647
|
-
|
|
9648
|
-
|
|
9649
|
-
|
|
9650
|
-
|
|
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
|
-
|
|
9654
|
-
|
|
9655
|
-
|
|
9656
|
-
|
|
9657
|
-
|
|
9658
|
-
|
|
9659
|
-
|
|
9660
|
-
|
|
9661
|
-
|
|
9662
|
-
|
|
9663
|
-
|
|
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
|
-
|
|
9667
|
-
|
|
9668
|
-
|
|
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
|
-
|
|
9671
|
-
|
|
9672
|
-
|
|
9673
|
-
|
|
9674
|
-
|
|
9675
|
-
|
|
9676
|
-
|
|
9677
|
-
|
|
9678
|
-
|
|
9679
|
-
}
|
|
9680
|
-
|
|
9681
|
-
|
|
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
|
-
|
|
9733
|
-
|
|
9734
|
-
|
|
9735
|
-
|
|
9736
|
-
|
|
9737
|
-
|
|
9738
|
-
|
|
9739
|
-
|
|
9740
|
-
|
|
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
|
-
|
|
9761
|
-
|
|
9762
|
-
|
|
9763
|
-
|
|
9764
|
-
|
|
9765
|
-
|
|
9766
|
-
|
|
9767
|
-
|
|
9768
|
-
|
|
9769
|
-
|
|
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
|
-
|
|
9775
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
10537
|
-
|
|
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
|
-
|
|
11166
|
-
|
|
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:
|
|
11527
|
+
reason: guardMessage,
|
|
11172
11528
|
riskFlags: ["orchestrator-contract"],
|
|
11173
11529
|
source: "orchestrator-guard",
|
|
11174
|
-
escalationMessage:
|
|
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}`);
|