@dv.nghiem/flowdeck 0.5.4 → 0.5.6
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 +19 -4
- package/dist/hooks/orchestrator-guard-hook.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2023 -1697
- 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,32 +1206,63 @@ 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
|
-
|
|
1232
|
-
|
|
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.
|
|
1232
|
+
|
|
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.
|
|
1237
|
+
|
|
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
|
|
1249
|
+
|
|
1250
|
+
Before delegating, you MUST emit a routing decision in this exact format:
|
|
1251
|
+
|
|
1252
|
+
\`\`\`
|
|
1253
|
+
## Routing Decision
|
|
1254
|
+
|
|
1255
|
+
**Request:** <brief summary of user request>
|
|
1256
|
+
**Classification:** <task type> | Confidence: <0.0-1.0>
|
|
1257
|
+
**Workflow Selected:** <workflow class>
|
|
1258
|
+
**Next Stage:** <stage name from Workflow Decision>
|
|
1259
|
+
**Reason:** <why this workflow and stage were chosen>
|
|
1260
|
+
**Execution Path:** <which agent(s) will execute>
|
|
1261
|
+
**Estimated Blast Radius:** <number of files or "unknown">
|
|
1262
|
+
\`\`\`
|
|
1263
|
+
|
|
1264
|
+
## Workflow Classes
|
|
1233
1265
|
|
|
1234
|
-
### Step 3: Choose Workflow
|
|
1235
1266
|
Select ONE of these workflow classes:
|
|
1236
1267
|
|
|
1237
1268
|
| Workflow Class | Execution Path | When to Select |
|
|
@@ -1244,26 +1275,29 @@ Select ONE of these workflow classes:
|
|
|
1244
1275
|
| \`docs-only\` | Route to @default-executor with \`inspect-only\` or \`simple-edit\` mode, or @writer for large docs | Documentation-only changes |
|
|
1245
1276
|
| \`verify-heavy\` | Plan with @planner (enhanced checks) → Execute with specialists → Verify with @reviewer + @security-auditor | High blast radius or sensitive paths |
|
|
1246
1277
|
|
|
1247
|
-
|
|
1248
|
-
Before routing, you MUST emit a routing decision in this exact format:
|
|
1278
|
+
## How to delegate
|
|
1249
1279
|
|
|
1250
|
-
|
|
1251
|
-
## Routing Decision
|
|
1280
|
+
Use \`@agent-name\` mentions to route work. Always include complete context — the subagent has no access to your reasoning:
|
|
1252
1281
|
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
\`\`\`
|
|
1282
|
+
@backend-coder
|
|
1283
|
+
Task: <exact description>
|
|
1284
|
+
Files: <targets>
|
|
1285
|
+
Constraints: <constraints>
|
|
1286
|
+
Acceptance criteria: <done definition>
|
|
1287
|
+
Context: <relevant findings>
|
|
1260
1288
|
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1289
|
+
## Recovery when a tool is blocked by the guard
|
|
1290
|
+
|
|
1291
|
+
If you see \`[Orchestrator Guard]\`, do NOT retry the same tool. Switch immediately to \`@agent-name\` delegation. The guard message lists available agents — pick the appropriate one and delegate with full context.
|
|
1292
|
+
|
|
1293
|
+
## Recovery when an agent returns no output or fails
|
|
1294
|
+
|
|
1295
|
+
1. Retry ONCE with more specific task description and explicit file targets.
|
|
1296
|
+
2. If it fails again, report to the human with the exact blocker. Do NOT loop.
|
|
1297
|
+
|
|
1298
|
+
## You must never stop with "blocked" without first attempting @agent delegation
|
|
1299
|
+
|
|
1300
|
+
If your current approach is blocked, \`@agent\` delegation is always available as an alternative path forward. Only escalate to the human if delegation itself fails twice.
|
|
1267
1301
|
|
|
1268
1302
|
## What You MAY Do Directly
|
|
1269
1303
|
|
|
@@ -1276,6 +1310,8 @@ You may ONLY use these tools directly:
|
|
|
1276
1310
|
- **decision-trace** — Record decisions
|
|
1277
1311
|
- **policy-engine** — Check policies
|
|
1278
1312
|
- **reflect** — Gather session artifacts
|
|
1313
|
+
- **load-rules / list-rules** — Discover agent capabilities and rules
|
|
1314
|
+
- **council** — Synthesize consensus across specialists
|
|
1279
1315
|
|
|
1280
1316
|
You may NEVER use:
|
|
1281
1317
|
- write, write_file, create, create_file
|
|
@@ -1306,6 +1342,9 @@ When workflow class is \`standard\`, \`explore\`, \`ui-heavy\`, \`bugfix\`, or \
|
|
|
1306
1342
|
- \`@reviewer\` — code quality review
|
|
1307
1343
|
- \`@security-auditor\` — security review
|
|
1308
1344
|
- \`@debug-specialist\` — root cause analysis
|
|
1345
|
+
- \`@build-error-resolver\` — build/type errors
|
|
1346
|
+
- \`@performance-optimizer\` — performance bottlenecks
|
|
1347
|
+
- \`@refactor-guide\` — safe refactoring
|
|
1309
1348
|
|
|
1310
1349
|
### Parallel Execution Patterns
|
|
1311
1350
|
|
|
@@ -6342,101 +6381,613 @@ import { join as join19, basename as basename2 } from "path";
|
|
|
6342
6381
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
6343
6382
|
import { dirname as dirname3 } from "path";
|
|
6344
6383
|
|
|
6345
|
-
// src/services/
|
|
6346
|
-
|
|
6347
|
-
|
|
6348
|
-
|
|
6349
|
-
|
|
6350
|
-
|
|
6351
|
-
|
|
6352
|
-
|
|
6353
|
-
|
|
6354
|
-
|
|
6355
|
-
|
|
6356
|
-
|
|
6357
|
-
|
|
6358
|
-
|
|
6359
|
-
|
|
6360
|
-
|
|
6361
|
-
|
|
6362
|
-
|
|
6363
|
-
|
|
6364
|
-
|
|
6365
|
-
|
|
6384
|
+
// src/services/preflight-explorer.ts
|
|
6385
|
+
var QUESTION_KIND_PATTERNS = [
|
|
6386
|
+
{
|
|
6387
|
+
kind: "what-tech-stack",
|
|
6388
|
+
patterns: ["tech stack", "language", "framework", "what are you using", "what tech", "built with", "written in"]
|
|
6389
|
+
},
|
|
6390
|
+
{
|
|
6391
|
+
kind: "is-project-initialized",
|
|
6392
|
+
patterns: ["initialized", "set up", "codebase mapped", "map-codebase", "new feature"]
|
|
6393
|
+
},
|
|
6394
|
+
{
|
|
6395
|
+
kind: "what-is-current-phase",
|
|
6396
|
+
patterns: ["current phase", "which phase", "what phase", "where are we", "current state"]
|
|
6397
|
+
},
|
|
6398
|
+
{
|
|
6399
|
+
kind: "what-patterns-exist",
|
|
6400
|
+
patterns: ["existing pattern", "how is it done", "how does the codebase", "pattern used", "architecture"]
|
|
6401
|
+
},
|
|
6402
|
+
{
|
|
6403
|
+
kind: "is-ui-heavy",
|
|
6404
|
+
patterns: ["ui", "frontend", "user interface", "webpage", "web app", "dashboard", "landing page", "screen"]
|
|
6405
|
+
},
|
|
6406
|
+
{
|
|
6407
|
+
kind: "has-existing-tests",
|
|
6408
|
+
patterns: ["test", "spec", "coverage", "tdd", "regression"]
|
|
6409
|
+
},
|
|
6410
|
+
{
|
|
6411
|
+
kind: "has-existing-docs",
|
|
6412
|
+
patterns: ["docs", "documentation", "readme", "api docs"]
|
|
6413
|
+
},
|
|
6414
|
+
{
|
|
6415
|
+
kind: "has-ci-cd",
|
|
6416
|
+
patterns: ["ci/cd", "continuous integration", "deploy", "pipeline", "github actions", ".github/workflow"]
|
|
6417
|
+
},
|
|
6418
|
+
{
|
|
6419
|
+
kind: "what-agents-available",
|
|
6420
|
+
patterns: ["which agent", "available agent", "what agent"]
|
|
6421
|
+
},
|
|
6422
|
+
{
|
|
6423
|
+
kind: "what-commands-available",
|
|
6424
|
+
patterns: ["which command", "available command", "what command", "slash command"]
|
|
6425
|
+
},
|
|
6426
|
+
{
|
|
6427
|
+
kind: "what-skills-available",
|
|
6428
|
+
patterns: ["skill", "available skill"]
|
|
6429
|
+
},
|
|
6430
|
+
{
|
|
6431
|
+
kind: "has-prior-decisions",
|
|
6432
|
+
patterns: ["prior decision", "previous discussion", "what was decided", "earlier session", "previous phase"]
|
|
6433
|
+
},
|
|
6434
|
+
{
|
|
6435
|
+
kind: "has-governance",
|
|
6436
|
+
patterns: ["governance", "policy", "approval", "supervisor"]
|
|
6366
6437
|
}
|
|
6367
|
-
|
|
6368
|
-
|
|
6369
|
-
|
|
6370
|
-
|
|
6371
|
-
|
|
6438
|
+
];
|
|
6439
|
+
function shouldSuppressQuestion(question, result, sessionHistory) {
|
|
6440
|
+
const lower = question.toLowerCase();
|
|
6441
|
+
const alreadyAsked = sessionHistory.some((h) => h.toLowerCase().trim() === lower.trim());
|
|
6442
|
+
if (alreadyAsked) {
|
|
6443
|
+
return {
|
|
6444
|
+
suppress: true,
|
|
6445
|
+
reason: "This question was already asked in the current session."
|
|
6446
|
+
};
|
|
6372
6447
|
}
|
|
6373
|
-
|
|
6374
|
-
|
|
6375
|
-
|
|
6448
|
+
const matchedEvidence = result.evidenceItems.filter((ev) => {
|
|
6449
|
+
const qKind = classifyQuestionKind(lower);
|
|
6450
|
+
return qKind !== null && qKind === ev.answersQuestion;
|
|
6451
|
+
});
|
|
6452
|
+
if (matchedEvidence.length > 0) {
|
|
6453
|
+
return {
|
|
6454
|
+
suppress: true,
|
|
6455
|
+
answeredByEvidence: true,
|
|
6456
|
+
reason: `Answered by repo evidence: ${matchedEvidence.map((e) => e.summary).join("; ")}`,
|
|
6457
|
+
evidence: matchedEvidence
|
|
6458
|
+
};
|
|
6376
6459
|
}
|
|
6460
|
+
return { suppress: false };
|
|
6377
6461
|
}
|
|
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;
|
|
6462
|
+
function refineClassification(clarificationPrompt, result, sessionHistory) {
|
|
6463
|
+
const promptLower = clarificationPrompt.toLowerCase();
|
|
6464
|
+
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"));
|
|
6465
|
+
if (isTaskTypeQuestion) {
|
|
6466
|
+
return {
|
|
6467
|
+
clarificationStillNeeded: false,
|
|
6468
|
+
resolvedReason: "PROJECT.md exists — project is initialized. Defaulting ambiguous task to feature."
|
|
6469
|
+
};
|
|
6391
6470
|
}
|
|
6392
|
-
|
|
6393
|
-
|
|
6471
|
+
const suppress = shouldSuppressQuestion(clarificationPrompt, result, sessionHistory);
|
|
6472
|
+
if (suppress.suppress) {
|
|
6473
|
+
return {
|
|
6474
|
+
clarificationStillNeeded: false,
|
|
6475
|
+
resolvedReason: suppress.reason
|
|
6476
|
+
};
|
|
6394
6477
|
}
|
|
6395
|
-
|
|
6396
|
-
|
|
6397
|
-
|
|
6398
|
-
|
|
6399
|
-
|
|
6400
|
-
|
|
6401
|
-
|
|
6402
|
-
|
|
6403
|
-
|
|
6404
|
-
|
|
6478
|
+
const lines = [];
|
|
6479
|
+
if (result.hasProjectMD)
|
|
6480
|
+
lines.push("PROJECT.md is present (project is initialized).");
|
|
6481
|
+
if (result.hasStateMD)
|
|
6482
|
+
lines.push("STATE.md is present (project has active session).");
|
|
6483
|
+
if (result.hasPriorDiscussions)
|
|
6484
|
+
lines.push("Prior DISCUSS.md files exist.");
|
|
6485
|
+
if (result.techStack.length > 0)
|
|
6486
|
+
lines.push(`Tech stack: ${result.techStack.join(", ")}.`);
|
|
6487
|
+
if (result.implementationPatterns.length > 0) {
|
|
6488
|
+
lines.push(`Implementation patterns: ${result.implementationPatterns.join(", ")}.`);
|
|
6489
|
+
}
|
|
6490
|
+
return {
|
|
6491
|
+
clarificationStillNeeded: true,
|
|
6492
|
+
supervisorContext: lines.length > 0 ? lines.join(" ") : undefined
|
|
6405
6493
|
};
|
|
6406
6494
|
}
|
|
6407
|
-
|
|
6408
|
-
|
|
6409
|
-
|
|
6410
|
-
|
|
6411
|
-
|
|
6412
|
-
|
|
6413
|
-
|
|
6414
|
-
|
|
6415
|
-
|
|
6416
|
-
|
|
6417
|
-
|
|
6495
|
+
var STOP_WORDS = new Set([
|
|
6496
|
+
"with",
|
|
6497
|
+
"that",
|
|
6498
|
+
"this",
|
|
6499
|
+
"from",
|
|
6500
|
+
"into",
|
|
6501
|
+
"when",
|
|
6502
|
+
"then",
|
|
6503
|
+
"will",
|
|
6504
|
+
"have",
|
|
6505
|
+
"been",
|
|
6506
|
+
"does",
|
|
6507
|
+
"should",
|
|
6508
|
+
"would",
|
|
6509
|
+
"could",
|
|
6510
|
+
"after",
|
|
6511
|
+
"before",
|
|
6512
|
+
"about"
|
|
6513
|
+
]);
|
|
6514
|
+
function classifyQuestionKind(questionLower) {
|
|
6515
|
+
for (const { kind, patterns } of QUESTION_KIND_PATTERNS) {
|
|
6516
|
+
if (patterns.some((p) => questionLower.includes(p)))
|
|
6517
|
+
return kind;
|
|
6518
|
+
}
|
|
6519
|
+
return null;
|
|
6418
6520
|
}
|
|
6419
|
-
|
|
6420
|
-
|
|
6521
|
+
|
|
6522
|
+
// src/services/workflow-router.ts
|
|
6523
|
+
function stage(name, command, requiresApproval, skippable, args) {
|
|
6524
|
+
return { name, command, args, requiresApproval, skippable };
|
|
6525
|
+
}
|
|
6526
|
+
function scoreTaskForRouting(criteria) {
|
|
6527
|
+
const simplicity = (criteria.taskType === "simple" ? 1 : 0) * 0.3;
|
|
6528
|
+
const confidence = criteria.confidence * 0.2;
|
|
6529
|
+
const lowRisk = !criteria.isSensitive && criteria.blastRadius < 3 ? 0.2 : 0;
|
|
6530
|
+
const knownCodebase = criteria.codebaseFreshness === "fresh" ? 0.15 : 0;
|
|
6531
|
+
const cheapComplexity = criteria.complexity === "cheap" ? 0.15 : 0;
|
|
6532
|
+
const total = simplicity + confidence + lowRisk + knownCodebase + cheapComplexity;
|
|
6421
6533
|
return {
|
|
6422
|
-
|
|
6423
|
-
|
|
6424
|
-
|
|
6425
|
-
|
|
6426
|
-
|
|
6427
|
-
|
|
6428
|
-
cacheWriteTokens: u.cacheWriteTokens,
|
|
6429
|
-
messageCount: u.messageCount
|
|
6534
|
+
simplicity,
|
|
6535
|
+
confidence,
|
|
6536
|
+
lowRisk,
|
|
6537
|
+
knownCodebase,
|
|
6538
|
+
cheapComplexity,
|
|
6539
|
+
total
|
|
6430
6540
|
};
|
|
6431
6541
|
}
|
|
6432
|
-
function
|
|
6433
|
-
|
|
6434
|
-
|
|
6435
|
-
|
|
6436
|
-
|
|
6437
|
-
|
|
6438
|
-
|
|
6439
|
-
|
|
6542
|
+
function buildAdaptiveStageSequence(criteria) {
|
|
6543
|
+
const scores = scoreTaskForRouting(criteria);
|
|
6544
|
+
const totalScore = scores.total;
|
|
6545
|
+
let workflowClass;
|
|
6546
|
+
let stages;
|
|
6547
|
+
let reason;
|
|
6548
|
+
if (totalScore >= 0.75 && (criteria.taskType === "simple" || criteria.taskType === "docs")) {
|
|
6549
|
+
workflowClass = "quick";
|
|
6550
|
+
stages = [
|
|
6551
|
+
stage("execute", "fd-execute", false, true),
|
|
6552
|
+
stage("verify", "fd-verify", false, true)
|
|
6553
|
+
];
|
|
6554
|
+
reason = `Quick workflow: score ${totalScore.toFixed(2)} >= 0.75 for ${criteria.taskType} task`;
|
|
6555
|
+
} else if (criteria.taskType === "bugfix") {
|
|
6556
|
+
workflowClass = "bugfix";
|
|
6557
|
+
stages = [
|
|
6558
|
+
stage("discuss", "fd-discuss", false, false),
|
|
6559
|
+
stage("fix-bug", "fd-fix-bug", false, false),
|
|
6560
|
+
stage("verify", "fd-verify", false, false)
|
|
6561
|
+
];
|
|
6562
|
+
reason = "Bugfix workflow: task type is bugfix";
|
|
6563
|
+
} else if (criteria.taskType === "docs" && totalScore < 0.75) {
|
|
6564
|
+
workflowClass = "docs-only";
|
|
6565
|
+
stages = [
|
|
6566
|
+
stage("write-docs", "fd-write-docs", false, false),
|
|
6567
|
+
stage("verify", "fd-verify", false, true)
|
|
6568
|
+
];
|
|
6569
|
+
reason = `Docs-only workflow: score ${totalScore.toFixed(2)} < 0.75 for docs task`;
|
|
6570
|
+
} else if (criteria.taskType === "ui-feature") {
|
|
6571
|
+
workflowClass = "ui-heavy";
|
|
6572
|
+
stages = [
|
|
6573
|
+
stage("discuss", "fd-discuss", false, false),
|
|
6574
|
+
stage("design", "fd-design", false, false, "--mode=draft"),
|
|
6575
|
+
stage("plan", "fd-plan", true, false),
|
|
6576
|
+
stage("execute", "fd-execute", false, false),
|
|
6577
|
+
stage("verify", "fd-verify", false, false)
|
|
6578
|
+
];
|
|
6579
|
+
reason = "UI-heavy workflow: task type indicates UI-heavy work";
|
|
6580
|
+
} else if (criteria.blastRadius >= 5 || criteria.isSensitive) {
|
|
6581
|
+
workflowClass = "verify-heavy";
|
|
6582
|
+
stages = [
|
|
6583
|
+
stage("plan", "fd-plan", true, false),
|
|
6584
|
+
stage("execute", "fd-execute", false, false),
|
|
6585
|
+
stage("verify", "fd-verify", false, false)
|
|
6586
|
+
];
|
|
6587
|
+
reason = `Verify-heavy workflow: blastRadius=${criteria.blastRadius}, isSensitive=${criteria.isSensitive}`;
|
|
6588
|
+
} else if (criteria.confidence < 0.6 || criteria.taskType === "ambiguous") {
|
|
6589
|
+
workflowClass = "explore";
|
|
6590
|
+
stages = [
|
|
6591
|
+
stage("discuss", "fd-discuss", false, false),
|
|
6592
|
+
stage("plan", "fd-plan", true, false),
|
|
6593
|
+
stage("execute", "fd-execute", false, false),
|
|
6594
|
+
stage("verify", "fd-verify", false, false)
|
|
6595
|
+
];
|
|
6596
|
+
reason = `Explore workflow: confidence=${criteria.confidence}, taskType=${criteria.taskType}`;
|
|
6597
|
+
} else {
|
|
6598
|
+
workflowClass = "standard";
|
|
6599
|
+
stages = [
|
|
6600
|
+
stage("plan", "fd-plan", true, false),
|
|
6601
|
+
stage("execute", "fd-execute", false, false),
|
|
6602
|
+
stage("verify", "fd-verify", false, false)
|
|
6603
|
+
];
|
|
6604
|
+
reason = `Standard workflow: score ${totalScore.toFixed(2)} with taskType ${criteria.taskType}`;
|
|
6605
|
+
}
|
|
6606
|
+
return {
|
|
6607
|
+
workflowClass,
|
|
6608
|
+
stages,
|
|
6609
|
+
criteria,
|
|
6610
|
+
scores,
|
|
6611
|
+
reason
|
|
6612
|
+
};
|
|
6613
|
+
}
|
|
6614
|
+
|
|
6615
|
+
// src/services/quick-router.ts
|
|
6616
|
+
var BUG_SIGNALS = [
|
|
6617
|
+
"fix",
|
|
6618
|
+
"bug",
|
|
6619
|
+
"broken",
|
|
6620
|
+
"not working",
|
|
6621
|
+
"doesn't work",
|
|
6622
|
+
"does not work",
|
|
6623
|
+
"error",
|
|
6624
|
+
"crash",
|
|
6625
|
+
"regression",
|
|
6626
|
+
"debug",
|
|
6627
|
+
"exception",
|
|
6628
|
+
"failing",
|
|
6629
|
+
"fails",
|
|
6630
|
+
"incorrect",
|
|
6631
|
+
"wrong output",
|
|
6632
|
+
"infinite loop",
|
|
6633
|
+
"null pointer",
|
|
6634
|
+
"undefined",
|
|
6635
|
+
"404",
|
|
6636
|
+
"500",
|
|
6637
|
+
"stack trace",
|
|
6638
|
+
"traceback",
|
|
6639
|
+
"root cause",
|
|
6640
|
+
"why is"
|
|
6641
|
+
];
|
|
6642
|
+
var UI_SIGNALS = [
|
|
6643
|
+
"landing page",
|
|
6644
|
+
"dashboard",
|
|
6645
|
+
"admin panel",
|
|
6646
|
+
"admin page",
|
|
6647
|
+
"app screen",
|
|
6648
|
+
"onboarding",
|
|
6649
|
+
"onboard",
|
|
6650
|
+
"wireframe",
|
|
6651
|
+
"mockup",
|
|
6652
|
+
"design system",
|
|
6653
|
+
"component library",
|
|
6654
|
+
"ui component",
|
|
6655
|
+
"ux flow",
|
|
6656
|
+
"user interface",
|
|
6657
|
+
"web app",
|
|
6658
|
+
"web application",
|
|
6659
|
+
"website",
|
|
6660
|
+
"frontend page",
|
|
6661
|
+
"mobile screen",
|
|
6662
|
+
"login page",
|
|
6663
|
+
"signup page",
|
|
6664
|
+
"settings page",
|
|
6665
|
+
"profile page",
|
|
6666
|
+
"modal",
|
|
6667
|
+
"dialog",
|
|
6668
|
+
"sidebar",
|
|
6669
|
+
"navigation",
|
|
6670
|
+
"navbar",
|
|
6671
|
+
"header",
|
|
6672
|
+
"footer",
|
|
6673
|
+
"layout",
|
|
6674
|
+
"responsive",
|
|
6675
|
+
"accessibility",
|
|
6676
|
+
"a11y",
|
|
6677
|
+
"dark mode",
|
|
6678
|
+
"theme"
|
|
6679
|
+
];
|
|
6680
|
+
var DOCS_SIGNALS = [
|
|
6681
|
+
"docs",
|
|
6682
|
+
"documentation",
|
|
6683
|
+
"readme",
|
|
6684
|
+
"api docs",
|
|
6685
|
+
"usage guide",
|
|
6686
|
+
"write docs",
|
|
6687
|
+
"document",
|
|
6688
|
+
"document the",
|
|
6689
|
+
"how to use",
|
|
6690
|
+
"tutorial",
|
|
6691
|
+
"changelog",
|
|
6692
|
+
"contributing guide",
|
|
6693
|
+
"docstring",
|
|
6694
|
+
"jsdoc",
|
|
6695
|
+
"tsdoc"
|
|
6696
|
+
];
|
|
6697
|
+
var SIMPLE_SIGNALS = [
|
|
6698
|
+
"rename",
|
|
6699
|
+
"move file",
|
|
6700
|
+
"quick",
|
|
6701
|
+
"minor",
|
|
6702
|
+
"small change",
|
|
6703
|
+
"one-liner",
|
|
6704
|
+
"typo",
|
|
6705
|
+
"update constant",
|
|
6706
|
+
"update config",
|
|
6707
|
+
"bump version"
|
|
6708
|
+
];
|
|
6709
|
+
var AMBIGUOUS_PATTERNS = [
|
|
6710
|
+
/^(improve|make|update|change|add|remove|help|do|run|check|use)\s+\w+$/i
|
|
6711
|
+
];
|
|
6712
|
+
function classifyTask(description) {
|
|
6713
|
+
const lower = description.toLowerCase().trim();
|
|
6714
|
+
if (!lower) {
|
|
6715
|
+
return _ambiguous([], "What task do you want to run? Please describe what you need done.");
|
|
6716
|
+
}
|
|
6717
|
+
const bugHits = BUG_SIGNALS.filter((s) => lower.includes(s));
|
|
6718
|
+
const uiHits = UI_SIGNALS.filter((s) => lower.includes(s));
|
|
6719
|
+
const docsHits = DOCS_SIGNALS.filter((s) => lower.includes(s));
|
|
6720
|
+
const simpleHits = SIMPLE_SIGNALS.filter((s) => lower.includes(s));
|
|
6721
|
+
const bugScore = Math.min(bugHits.length * 0.35, 1);
|
|
6722
|
+
const uiScore = Math.min(uiHits.length * 0.3, 1);
|
|
6723
|
+
const docsScore = Math.min(docsHits.length * 0.4, 1);
|
|
6724
|
+
const simpleScore = Math.min(simpleHits.length * 0.45, 1);
|
|
6725
|
+
if (bugScore >= 0.35 && bugScore >= uiScore && bugScore >= docsScore) {
|
|
6726
|
+
return {
|
|
6727
|
+
taskType: "bugfix",
|
|
6728
|
+
confidence: Math.min(0.5 + bugScore * 0.5, 0.98),
|
|
6729
|
+
signals: bugHits,
|
|
6730
|
+
requiresDesign: false,
|
|
6731
|
+
requiresTDD: true,
|
|
6732
|
+
stageSequence: buildStageSequence("bugfix"),
|
|
6733
|
+
clarificationNeeded: bugScore < 0.5,
|
|
6734
|
+
clarificationPrompt: bugScore < 0.5 ? "Can you describe the specific bug? What is the expected vs actual behavior?" : undefined
|
|
6735
|
+
};
|
|
6736
|
+
}
|
|
6737
|
+
if (uiScore >= 0.3) {
|
|
6738
|
+
return {
|
|
6739
|
+
taskType: "ui-feature",
|
|
6740
|
+
confidence: Math.min(0.5 + uiScore * 0.45, 0.95),
|
|
6741
|
+
signals: uiHits,
|
|
6742
|
+
requiresDesign: true,
|
|
6743
|
+
requiresTDD: true,
|
|
6744
|
+
stageSequence: buildStageSequence("ui-feature"),
|
|
6745
|
+
clarificationNeeded: false
|
|
6746
|
+
};
|
|
6747
|
+
}
|
|
6748
|
+
if (docsScore >= 0.4 && docsScore >= bugScore) {
|
|
6749
|
+
return {
|
|
6750
|
+
taskType: "docs",
|
|
6751
|
+
confidence: Math.min(0.55 + docsScore * 0.4, 0.95),
|
|
6752
|
+
signals: docsHits,
|
|
6753
|
+
requiresDesign: false,
|
|
6754
|
+
requiresTDD: false,
|
|
6755
|
+
stageSequence: buildStageSequence("docs"),
|
|
6756
|
+
clarificationNeeded: false
|
|
6757
|
+
};
|
|
6758
|
+
}
|
|
6759
|
+
if (simpleScore >= 0.45) {
|
|
6760
|
+
return {
|
|
6761
|
+
taskType: "simple",
|
|
6762
|
+
confidence: Math.min(0.55 + simpleScore * 0.35, 0.9),
|
|
6763
|
+
signals: simpleHits,
|
|
6764
|
+
requiresDesign: false,
|
|
6765
|
+
requiresTDD: false,
|
|
6766
|
+
stageSequence: buildStageSequence("simple"),
|
|
6767
|
+
clarificationNeeded: false
|
|
6768
|
+
};
|
|
6769
|
+
}
|
|
6770
|
+
const wordCount = lower.split(/\s+/).filter(Boolean).length;
|
|
6771
|
+
if (wordCount >= 5) {
|
|
6772
|
+
return {
|
|
6773
|
+
taskType: "feature",
|
|
6774
|
+
confidence: Math.min(0.5 + wordCount * 0.02, 0.85),
|
|
6775
|
+
signals: [],
|
|
6776
|
+
requiresDesign: false,
|
|
6777
|
+
requiresTDD: true,
|
|
6778
|
+
stageSequence: buildStageSequence("feature"),
|
|
6779
|
+
clarificationNeeded: wordCount < 8,
|
|
6780
|
+
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
|
|
6781
|
+
};
|
|
6782
|
+
}
|
|
6783
|
+
const isAmbiguousPattern = AMBIGUOUS_PATTERNS.some((p) => p.test(lower));
|
|
6784
|
+
if (isAmbiguousPattern || wordCount < 5) {
|
|
6785
|
+
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?");
|
|
6786
|
+
}
|
|
6787
|
+
return {
|
|
6788
|
+
taskType: "feature",
|
|
6789
|
+
confidence: 0.6,
|
|
6790
|
+
signals: [],
|
|
6791
|
+
requiresDesign: false,
|
|
6792
|
+
requiresTDD: true,
|
|
6793
|
+
stageSequence: buildStageSequence("feature"),
|
|
6794
|
+
clarificationNeeded: false
|
|
6795
|
+
};
|
|
6796
|
+
}
|
|
6797
|
+
function _ambiguous(signals, prompt) {
|
|
6798
|
+
return {
|
|
6799
|
+
taskType: "ambiguous",
|
|
6800
|
+
confidence: 0,
|
|
6801
|
+
signals,
|
|
6802
|
+
requiresDesign: false,
|
|
6803
|
+
requiresTDD: false,
|
|
6804
|
+
stageSequence: [],
|
|
6805
|
+
clarificationNeeded: true,
|
|
6806
|
+
clarificationPrompt: prompt
|
|
6807
|
+
};
|
|
6808
|
+
}
|
|
6809
|
+
function buildStageSequence(taskType) {
|
|
6810
|
+
switch (taskType) {
|
|
6811
|
+
case "feature":
|
|
6812
|
+
return [
|
|
6813
|
+
stage2("discuss", "fd-discuss", false, false),
|
|
6814
|
+
stage2("plan", "fd-plan", true, false),
|
|
6815
|
+
stage2("execute", "fd-execute", false, false),
|
|
6816
|
+
stage2("verify", "fd-verify", false, false)
|
|
6817
|
+
];
|
|
6818
|
+
case "ui-feature":
|
|
6819
|
+
return [
|
|
6820
|
+
stage2("discuss", "fd-discuss", false, false),
|
|
6821
|
+
stage2("design", "fd-design", false, false, "--mode=draft"),
|
|
6822
|
+
stage2("plan", "fd-plan", true, false),
|
|
6823
|
+
stage2("execute", "fd-execute", false, false),
|
|
6824
|
+
stage2("verify", "fd-verify", false, false)
|
|
6825
|
+
];
|
|
6826
|
+
case "bugfix":
|
|
6827
|
+
return [
|
|
6828
|
+
stage2("discuss", "fd-discuss", false, false),
|
|
6829
|
+
stage2("fix-bug", "fd-fix-bug", false, false),
|
|
6830
|
+
stage2("verify", "fd-verify", false, false)
|
|
6831
|
+
];
|
|
6832
|
+
case "docs":
|
|
6833
|
+
return [
|
|
6834
|
+
stage2("discuss", "fd-discuss", false, false),
|
|
6835
|
+
stage2("write-docs", "fd-write-docs", false, false),
|
|
6836
|
+
stage2("verify", "fd-verify", false, true)
|
|
6837
|
+
];
|
|
6838
|
+
case "simple":
|
|
6839
|
+
return [
|
|
6840
|
+
stage2("execute", "fd-execute", false, false),
|
|
6841
|
+
stage2("verify", "fd-verify", false, true)
|
|
6842
|
+
];
|
|
6843
|
+
case "ambiguous":
|
|
6844
|
+
return [];
|
|
6845
|
+
default:
|
|
6846
|
+
return [];
|
|
6847
|
+
}
|
|
6848
|
+
}
|
|
6849
|
+
function buildAdaptiveWorkflow(description, exploration) {
|
|
6850
|
+
const base = exploration ? classifyTaskWithContext(description, exploration) : classifyTask(description);
|
|
6851
|
+
const complexityResult = classifyTaskComplexity(description);
|
|
6852
|
+
const criteria = {
|
|
6853
|
+
taskType: base.taskType,
|
|
6854
|
+
complexity: complexityResult.complexity,
|
|
6855
|
+
confidence: base.confidence,
|
|
6856
|
+
blastRadius: 0,
|
|
6857
|
+
isSensitive: false,
|
|
6858
|
+
codebaseFreshness: exploration ? "fresh" : "unknown",
|
|
6859
|
+
requiresTests: base.requiresTDD
|
|
6860
|
+
};
|
|
6861
|
+
const route = buildAdaptiveStageSequence(criteria);
|
|
6862
|
+
return {
|
|
6863
|
+
...base,
|
|
6864
|
+
stageSequence: route.stages,
|
|
6865
|
+
workflowClass: route.workflowClass,
|
|
6866
|
+
scores: route.scores
|
|
6867
|
+
};
|
|
6868
|
+
}
|
|
6869
|
+
function stage2(name, command, requiresApproval, skippable, args) {
|
|
6870
|
+
return { name, command, args, requiresApproval, skippable };
|
|
6871
|
+
}
|
|
6872
|
+
function classifyTaskWithContext(description, exploration, sessionHistory = []) {
|
|
6873
|
+
const base = classifyTask(description);
|
|
6874
|
+
if (!base.clarificationNeeded) {
|
|
6875
|
+
return base;
|
|
6876
|
+
}
|
|
6877
|
+
const refinement = refineClassification(base.clarificationPrompt ?? "", exploration, sessionHistory);
|
|
6878
|
+
if (!refinement.clarificationStillNeeded) {
|
|
6879
|
+
const resolvedType = base.taskType === "ambiguous" ? "feature" : base.taskType;
|
|
6880
|
+
return {
|
|
6881
|
+
...base,
|
|
6882
|
+
taskType: resolvedType,
|
|
6883
|
+
stageSequence: buildStageSequence(resolvedType),
|
|
6884
|
+
clarificationNeeded: false,
|
|
6885
|
+
clarificationPrompt: undefined,
|
|
6886
|
+
confidence: Math.max(base.confidence, 0.55)
|
|
6887
|
+
};
|
|
6888
|
+
}
|
|
6889
|
+
const enrichedPrompt = refinement.supervisorContext ? `${base.clarificationPrompt ?? ""} (Context: ${refinement.supervisorContext})` : base.clarificationPrompt;
|
|
6890
|
+
return {
|
|
6891
|
+
...base,
|
|
6892
|
+
clarificationPrompt: enrichedPrompt
|
|
6893
|
+
};
|
|
6894
|
+
}
|
|
6895
|
+
|
|
6896
|
+
// src/services/prompt-cache.ts
|
|
6897
|
+
import { createHash as createHash3 } from "crypto";
|
|
6898
|
+
var DEFAULT_PROMPT_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
6899
|
+
|
|
6900
|
+
class PromptCacheImpl {
|
|
6901
|
+
store = new Map;
|
|
6902
|
+
getKey(description, stage3, languages) {
|
|
6903
|
+
const normalized = description.trim().toLowerCase().replace(/\s+/g, " ");
|
|
6904
|
+
const sortedLanguages = [...languages].sort().join(",");
|
|
6905
|
+
const payload = `${normalized}|${stage3}|${sortedLanguages}`;
|
|
6906
|
+
return createHash3("sha256").update(payload).digest("hex");
|
|
6907
|
+
}
|
|
6908
|
+
get(key) {
|
|
6909
|
+
const entry = this.store.get(key);
|
|
6910
|
+
if (!entry)
|
|
6911
|
+
return;
|
|
6912
|
+
if (Date.now() >= entry.expiresAt) {
|
|
6913
|
+
this.store.delete(key);
|
|
6914
|
+
return;
|
|
6915
|
+
}
|
|
6916
|
+
return entry.fragment;
|
|
6917
|
+
}
|
|
6918
|
+
set(key, fragment, ttlMs = DEFAULT_PROMPT_CACHE_TTL_MS) {
|
|
6919
|
+
this.store.set(key, {
|
|
6920
|
+
fragment,
|
|
6921
|
+
expiresAt: Date.now() + Math.max(0, ttlMs)
|
|
6922
|
+
});
|
|
6923
|
+
}
|
|
6924
|
+
invalidate() {
|
|
6925
|
+
this.store.clear();
|
|
6926
|
+
invalidateCache();
|
|
6927
|
+
}
|
|
6928
|
+
}
|
|
6929
|
+
var sharedPromptCache = new PromptCacheImpl;
|
|
6930
|
+
|
|
6931
|
+
// src/services/token-metrics.ts
|
|
6932
|
+
var ESTIMATION_COEFFICIENTS = {
|
|
6933
|
+
prose: 4,
|
|
6934
|
+
code: 3.5,
|
|
6935
|
+
json: 3.5
|
|
6936
|
+
};
|
|
6937
|
+
var usageByRun = new Map;
|
|
6938
|
+
function toPositiveInt(value) {
|
|
6939
|
+
if (typeof value === "string") {
|
|
6940
|
+
const n = Number(value);
|
|
6941
|
+
return Number.isFinite(n) && n >= 0 ? Math.floor(n) : 0;
|
|
6942
|
+
}
|
|
6943
|
+
if (typeof value === "number" && Number.isFinite(value) && value >= 0) {
|
|
6944
|
+
return Math.floor(value);
|
|
6945
|
+
}
|
|
6946
|
+
return 0;
|
|
6947
|
+
}
|
|
6948
|
+
function getAccumulated(runId) {
|
|
6949
|
+
return usageByRun.get(runId) ?? {
|
|
6950
|
+
inputTokens: 0,
|
|
6951
|
+
outputTokens: 0,
|
|
6952
|
+
reasoningTokens: 0,
|
|
6953
|
+
cacheReadTokens: 0,
|
|
6954
|
+
cacheWriteTokens: 0,
|
|
6955
|
+
messageCount: 0
|
|
6956
|
+
};
|
|
6957
|
+
}
|
|
6958
|
+
function recordMessageUsage(runId, usage) {
|
|
6959
|
+
const current = getAccumulated(runId);
|
|
6960
|
+
const next = {
|
|
6961
|
+
inputTokens: current.inputTokens + toPositiveInt(usage.input),
|
|
6962
|
+
outputTokens: current.outputTokens + toPositiveInt(usage.output),
|
|
6963
|
+
reasoningTokens: current.reasoningTokens + toPositiveInt(usage.reasoning),
|
|
6964
|
+
cacheReadTokens: current.cacheReadTokens + toPositiveInt(usage.cache?.read),
|
|
6965
|
+
cacheWriteTokens: current.cacheWriteTokens + toPositiveInt(usage.cache?.write),
|
|
6966
|
+
messageCount: current.messageCount + 1
|
|
6967
|
+
};
|
|
6968
|
+
usageByRun.set(runId, next);
|
|
6969
|
+
}
|
|
6970
|
+
function getRunUsage(runId) {
|
|
6971
|
+
const u = getAccumulated(runId);
|
|
6972
|
+
return {
|
|
6973
|
+
runId,
|
|
6974
|
+
totalTokens: u.inputTokens + u.outputTokens + u.reasoningTokens + u.cacheReadTokens + u.cacheWriteTokens,
|
|
6975
|
+
inputTokens: u.inputTokens,
|
|
6976
|
+
outputTokens: u.outputTokens,
|
|
6977
|
+
reasoningTokens: u.reasoningTokens,
|
|
6978
|
+
cacheReadTokens: u.cacheReadTokens,
|
|
6979
|
+
cacheWriteTokens: u.cacheWriteTokens,
|
|
6980
|
+
messageCount: u.messageCount
|
|
6981
|
+
};
|
|
6982
|
+
}
|
|
6983
|
+
function estimateTokens(text, kind = "prose") {
|
|
6984
|
+
if (!text || text.length === 0)
|
|
6985
|
+
return 0;
|
|
6986
|
+
const coefficient = ESTIMATION_COEFFICIENTS[kind] ?? ESTIMATION_COEFFICIENTS.prose;
|
|
6987
|
+
return Math.max(0, Math.round(text.length / coefficient));
|
|
6988
|
+
}
|
|
6989
|
+
|
|
6990
|
+
// src/services/context-ingress.ts
|
|
6440
6991
|
var DEFAULT_OPTIONS = {
|
|
6441
6992
|
maxEvents: 20,
|
|
6442
6993
|
eventMaxAgeMinutes: 30,
|
|
@@ -6664,9 +7215,10 @@ class ContextIngressService {
|
|
|
6664
7215
|
const codebaseDocs = trivial.isTrivialChat ? {} : this.readCodebaseDocs(input.projectRoot);
|
|
6665
7216
|
const recentEvents = trivial.isTrivialChat ? [] : this.readRecentEvents(input.projectRoot);
|
|
6666
7217
|
const route = this.buildRoute(input.description);
|
|
7218
|
+
const workflowDecision = input.workflowDecision ?? buildAdaptiveWorkflow(input.description);
|
|
6667
7219
|
const relevantRules = trivial.isTrivialChat ? [] : this.selectRelevantRules(input.projectRoot, input.description, state, route, input.currentStage);
|
|
6668
7220
|
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");
|
|
7221
|
+
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
7222
|
const total = this.options.totalTokenBudget;
|
|
6671
7223
|
const actualUsage = getRunUsage(input.sessionId);
|
|
6672
7224
|
const hasActualTokens = actualUsage.totalTokens > 0;
|
|
@@ -6689,7 +7241,8 @@ class ContextIngressService {
|
|
|
6689
7241
|
recentEvents,
|
|
6690
7242
|
selectedRules: relevantRules,
|
|
6691
7243
|
selectedSkills: relevantSkills,
|
|
6692
|
-
tokenBudget
|
|
7244
|
+
tokenBudget,
|
|
7245
|
+
workflowDecision
|
|
6693
7246
|
};
|
|
6694
7247
|
this._assembledBySession.set(input.sessionId, ctx);
|
|
6695
7248
|
this.persistBudgetSnapshot(ctx);
|
|
@@ -6716,12 +7269,12 @@ class ContextIngressService {
|
|
|
6716
7269
|
}
|
|
6717
7270
|
}
|
|
6718
7271
|
buildPromptFragment(ctx) {
|
|
6719
|
-
const
|
|
7272
|
+
const stage3 = String(ctx.planningState.phase ?? "discuss");
|
|
6720
7273
|
if (ctx.isTrivialChat) {
|
|
6721
|
-
return `Greeting. Phase: ${
|
|
7274
|
+
return `Greeting. Phase: ${stage3}.`;
|
|
6722
7275
|
}
|
|
6723
7276
|
const languages = getCachedLanguages(ctx.directory);
|
|
6724
|
-
const cacheKey = sharedPromptCache.getKey(ctx.description,
|
|
7277
|
+
const cacheKey = sharedPromptCache.getKey(ctx.description, stage3, languages);
|
|
6725
7278
|
const cached = sharedPromptCache.get(cacheKey);
|
|
6726
7279
|
if (cached)
|
|
6727
7280
|
return cached;
|
|
@@ -6729,7 +7282,7 @@ class ContextIngressService {
|
|
|
6729
7282
|
"# FlowDeck Context",
|
|
6730
7283
|
"",
|
|
6731
7284
|
`Task: ${ctx.description}`,
|
|
6732
|
-
`Phase: ${
|
|
7285
|
+
`Phase: ${stage3}`,
|
|
6733
7286
|
"",
|
|
6734
7287
|
"## Selected rules"
|
|
6735
7288
|
];
|
|
@@ -6756,6 +7309,22 @@ class ContextIngressService {
|
|
|
6756
7309
|
lines.push(`blockers: ${ctx.planningState.blockers.join(", ")}`);
|
|
6757
7310
|
}
|
|
6758
7311
|
lines.push(`next_action: ${ctx.planningState.next_action}`);
|
|
7312
|
+
lines.push("", "## Workflow Decision");
|
|
7313
|
+
const decision = ctx.workflowDecision;
|
|
7314
|
+
if (decision) {
|
|
7315
|
+
lines.push(`workflowClass: ${decision.workflowClass ?? decision.taskType}`);
|
|
7316
|
+
lines.push(`taskType: ${decision.taskType}`);
|
|
7317
|
+
lines.push(`confidence: ${decision.confidence.toFixed(2)}`);
|
|
7318
|
+
lines.push(`requiresDesign: ${decision.requiresDesign}`);
|
|
7319
|
+
lines.push(`requiresTDD: ${decision.requiresTDD}`);
|
|
7320
|
+
lines.push(`stageSequence: ${decision.stageSequence.map((s) => s.name).join(" → ")}`);
|
|
7321
|
+
if (decision.clarificationNeeded) {
|
|
7322
|
+
lines.push(`clarificationNeeded: true`);
|
|
7323
|
+
lines.push(`clarificationPrompt: ${decision.clarificationPrompt ?? "(none provided)"}`);
|
|
7324
|
+
}
|
|
7325
|
+
} else {
|
|
7326
|
+
lines.push("_No workflow decision available._");
|
|
7327
|
+
}
|
|
6759
7328
|
lines.push("", "## Recent events");
|
|
6760
7329
|
if (ctx.recentEvents.length === 0) {
|
|
6761
7330
|
lines.push("_No recent events._");
|
|
@@ -6776,7 +7345,8 @@ class ContextIngressService {
|
|
|
6776
7345
|
const eventsTokens = estimateTokens(JSON.stringify(ctx.recentEvents), "json");
|
|
6777
7346
|
const rulesTokens = estimateTokens(JSON.stringify(ctx.selectedRules), "json");
|
|
6778
7347
|
const skillsTokens = estimateTokens(JSON.stringify(ctx.selectedSkills), "json");
|
|
6779
|
-
const
|
|
7348
|
+
const workflowTokens = estimateTokens(JSON.stringify(ctx.workflowDecision), "json");
|
|
7349
|
+
const totalFallbackTokens = stateTokens + planTokens + docsTokens + eventsTokens + rulesTokens + skillsTokens + workflowTokens;
|
|
6780
7350
|
const actualUsage = getRunUsage(ctx.sessionID);
|
|
6781
7351
|
const hasActualTokens = actualUsage.totalTokens > 0;
|
|
6782
7352
|
const totalUsed = hasActualTokens ? actualUsage.totalTokens : ctx.tokenBudget.usedTokens;
|
|
@@ -6805,7 +7375,8 @@ class ContextIngressService {
|
|
|
6805
7375
|
docs: makeSnapshot("docs", docsTokens),
|
|
6806
7376
|
events: makeSnapshot("events", eventsTokens),
|
|
6807
7377
|
rules: makeSnapshot("rules", rulesTokens),
|
|
6808
|
-
skills: makeSnapshot("skills", skillsTokens)
|
|
7378
|
+
skills: makeSnapshot("skills", skillsTokens),
|
|
7379
|
+
workflow: makeSnapshot("workflow", workflowTokens)
|
|
6809
7380
|
};
|
|
6810
7381
|
}
|
|
6811
7382
|
getTotalBudget(sessionID) {
|
|
@@ -6918,12 +7489,12 @@ class ContextIngressService {
|
|
|
6918
7489
|
const sharedDir = join19(__dir, "..", "agents", "shared");
|
|
6919
7490
|
if (!existsSync19(rulesDir))
|
|
6920
7491
|
return [];
|
|
6921
|
-
const
|
|
7492
|
+
const stage3 = currentStage ?? this.inferStageFromRoute(route);
|
|
6922
7493
|
const languages = getCachedLanguages(projectRoot);
|
|
6923
|
-
const cacheKey = `${projectRoot}\x00${
|
|
7494
|
+
const cacheKey = `${projectRoot}\x00${stage3 ?? ""}\x00${[...languages].sort().join(",")}`;
|
|
6924
7495
|
let selection = ruleSelectionCache.get(cacheKey);
|
|
6925
7496
|
if (!selection) {
|
|
6926
|
-
selection = selectRulePaths(rulesDir, { languages, stage });
|
|
7497
|
+
selection = selectRulePaths(rulesDir, { languages, stage: stage3 });
|
|
6927
7498
|
ruleSelectionCache.set(cacheKey, selection);
|
|
6928
7499
|
}
|
|
6929
7500
|
const seen = new Set;
|
|
@@ -7289,8 +7860,8 @@ class LoopDetector {
|
|
|
7289
7860
|
var CONTRACTS = [
|
|
7290
7861
|
{
|
|
7291
7862
|
agent: "orchestrator",
|
|
7292
|
-
role: "Coordinate multi-agent execution, inspect context directly, and route specialist work
|
|
7293
|
-
allowedTaskTypes: ["orchestration", "coordination", "
|
|
7863
|
+
role: "Coordinate multi-agent execution, inspect context directly, and route specialist work via native @agent-name mentions.",
|
|
7864
|
+
allowedTaskTypes: ["orchestration", "coordination", "routing", "phase-management"],
|
|
7294
7865
|
requiredInputs: ["STATE.md", "PLAN.md"],
|
|
7295
7866
|
expectedOutputFields: ["completed_steps", "current_phase"],
|
|
7296
7867
|
allowedTools: [
|
|
@@ -7597,43 +8168,283 @@ var CONTRACTS = [
|
|
|
7597
8168
|
expectedOutputFields: ["updated_docs"],
|
|
7598
8169
|
allowedTools: ["read", "write", "edit", "glob", "grep"],
|
|
7599
8170
|
forbiddenActions: [
|
|
7600
|
-
"modify application code",
|
|
7601
|
-
"delete documentation without replacement"
|
|
8171
|
+
"modify application code",
|
|
8172
|
+
"delete documentation without replacement"
|
|
8173
|
+
],
|
|
8174
|
+
escalationConditions: ["documentation conflicts with implementation"],
|
|
8175
|
+
stopConditions: ["docs updated and synced"],
|
|
8176
|
+
successCriteria: ["docs reflect current code", "no application code changed"]
|
|
8177
|
+
},
|
|
8178
|
+
{
|
|
8179
|
+
agent: "mapper",
|
|
8180
|
+
role: "Map and analyze codebase structure, dependencies, and call graphs.",
|
|
8181
|
+
allowedTaskTypes: ["codebase-mapping", "dependency-analysis", "call-graph"],
|
|
8182
|
+
requiredInputs: ["directory or module to map"],
|
|
8183
|
+
expectedOutputFields: ["structure_map", "dependencies", "summary"],
|
|
8184
|
+
allowedTools: ["read", "glob", "grep", "codegraph", "codegraph-search"],
|
|
8185
|
+
forbiddenActions: [
|
|
8186
|
+
"write or edit files",
|
|
8187
|
+
"run bash commands",
|
|
8188
|
+
"modify application source code"
|
|
8189
|
+
],
|
|
8190
|
+
escalationConditions: ["codebase too large to map in one pass"],
|
|
8191
|
+
stopConditions: ["map complete"],
|
|
8192
|
+
successCriteria: ["structured map output", "no file modifications"]
|
|
8193
|
+
},
|
|
8194
|
+
{
|
|
8195
|
+
agent: "supervisor",
|
|
8196
|
+
role: "Governance review layer. Inspects existing commands/agents, validates policy, returns structured approve/revise/block/escalate decision. Never creates new commands or workflows.",
|
|
8197
|
+
allowedTaskTypes: ["governance-review", "policy-check", "pre-execution-review", "post-stage-review"],
|
|
8198
|
+
requiredInputs: ["target name (command or agent)", "task context"],
|
|
8199
|
+
expectedOutputFields: ["decision", "targetType", "targetName", "exists", "reasons", "missingRequirements", "riskFlags", "requiredChanges", "approvalStatus", "confidenceScore"],
|
|
8200
|
+
allowedTools: ["read", "glob", "grep", "planning-state", "policy-engine"],
|
|
8201
|
+
forbiddenActions: [
|
|
8202
|
+
"create new commands",
|
|
8203
|
+
"create new workflows",
|
|
8204
|
+
"invent new agent names",
|
|
8205
|
+
"modify command intent",
|
|
8206
|
+
"replace orchestrator",
|
|
8207
|
+
"become second dispatcher",
|
|
8208
|
+
"execute implementation tasks",
|
|
8209
|
+
"write or edit source files",
|
|
8210
|
+
"run bash commands",
|
|
8211
|
+
"modify PLAN.md or STATE.md"
|
|
8212
|
+
],
|
|
8213
|
+
escalationConditions: [
|
|
8214
|
+
"human approval required and not granted",
|
|
8215
|
+
"confidence below threshold",
|
|
8216
|
+
"critical policy violation with no safe path forward"
|
|
8217
|
+
],
|
|
8218
|
+
stopConditions: ["structured decision issued", "review complete"],
|
|
8219
|
+
successCriteria: [
|
|
8220
|
+
"structured SupervisorDecision returned",
|
|
8221
|
+
"no new commands or workflows created",
|
|
8222
|
+
"existing registry not modified",
|
|
8223
|
+
"decision is one of: approve, revise, block, escalate"
|
|
8224
|
+
]
|
|
8225
|
+
},
|
|
8226
|
+
{
|
|
8227
|
+
agent: "default-executor",
|
|
8228
|
+
role: "General-purpose executor for simple direct tasks: renames, typo fixes, small edits.",
|
|
8229
|
+
allowedTaskTypes: ["simple-edit", "rename", "typo-fix", "quick-change"],
|
|
8230
|
+
requiredInputs: ["task description"],
|
|
8231
|
+
expectedOutputFields: ["files_modified", "summary"],
|
|
8232
|
+
allowedTools: ["read", "write", "edit", "bash", "glob", "grep"],
|
|
8233
|
+
forbiddenActions: [
|
|
8234
|
+
"architectural changes",
|
|
8235
|
+
"database migrations",
|
|
8236
|
+
"orchestrate other agents",
|
|
8237
|
+
"expand scope silently"
|
|
8238
|
+
],
|
|
8239
|
+
escalationConditions: ["task scope expands beyond simple edit"],
|
|
8240
|
+
stopConditions: ["task complete"],
|
|
8241
|
+
successCriteria: ["change made", "no regressions"]
|
|
8242
|
+
},
|
|
8243
|
+
{
|
|
8244
|
+
agent: "code-explorer",
|
|
8245
|
+
role: "Explore and map unfamiliar codebases before changes. Read-only analysis.",
|
|
8246
|
+
allowedTaskTypes: ["exploration", "code-mapping", "call-path-tracing"],
|
|
8247
|
+
requiredInputs: ["area or feature to explore"],
|
|
8248
|
+
expectedOutputFields: ["structure", "entry_points", "key_components", "call_paths"],
|
|
8249
|
+
allowedTools: [
|
|
8250
|
+
"read",
|
|
8251
|
+
"glob",
|
|
8252
|
+
"grep",
|
|
8253
|
+
"codegraph",
|
|
8254
|
+
"codegraph-search",
|
|
8255
|
+
"codegraph-node",
|
|
8256
|
+
"codegraph-explore"
|
|
8257
|
+
],
|
|
8258
|
+
forbiddenActions: [
|
|
8259
|
+
"modify files",
|
|
8260
|
+
"implement changes",
|
|
8261
|
+
"run destructive commands"
|
|
8262
|
+
],
|
|
8263
|
+
escalationConditions: ["codebase index missing and exploration blocked"],
|
|
8264
|
+
stopConditions: ["exploration report complete"],
|
|
8265
|
+
successCriteria: [
|
|
8266
|
+
"report delivered",
|
|
8267
|
+
"no files modified"
|
|
8268
|
+
]
|
|
8269
|
+
},
|
|
8270
|
+
{
|
|
8271
|
+
agent: "debug-specialist",
|
|
8272
|
+
role: "Diagnose bugs through systematic root cause analysis. Report only, do not implement fixes directly.",
|
|
8273
|
+
allowedTaskTypes: ["debugging", "root-cause-analysis", "bug-investigation"],
|
|
8274
|
+
requiredInputs: ["bug report or failure output"],
|
|
8275
|
+
expectedOutputFields: ["root_cause", "evidence", "call_path", "recommended_fix"],
|
|
8276
|
+
allowedTools: ["read", "glob", "grep", "bash"],
|
|
8277
|
+
forbiddenActions: [
|
|
8278
|
+
"implement fixes directly",
|
|
8279
|
+
"suppress errors without fixing",
|
|
8280
|
+
"modify application code"
|
|
8281
|
+
],
|
|
8282
|
+
escalationConditions: [
|
|
8283
|
+
"architectural problem found",
|
|
8284
|
+
"security issue discovered"
|
|
8285
|
+
],
|
|
8286
|
+
stopConditions: ["debug report complete"],
|
|
8287
|
+
successCriteria: [
|
|
8288
|
+
"root cause identified",
|
|
8289
|
+
"fix recommended",
|
|
8290
|
+
"no implementation done"
|
|
8291
|
+
]
|
|
8292
|
+
},
|
|
8293
|
+
{
|
|
8294
|
+
agent: "build-error-resolver",
|
|
8295
|
+
role: "Fix build errors, compilation failures, and dependency issues with minimal changes.",
|
|
8296
|
+
allowedTaskTypes: ["build-fix", "type-error-fix", "dependency-fix"],
|
|
8297
|
+
requiredInputs: ["build error output"],
|
|
8298
|
+
expectedOutputFields: ["files_modified", "summary", "verification"],
|
|
8299
|
+
allowedTools: ["read", "write", "edit", "bash", "glob", "grep"],
|
|
8300
|
+
forbiddenActions: [
|
|
8301
|
+
"use as any or @ts-ignore to suppress type errors",
|
|
8302
|
+
"refactor unrelated code",
|
|
8303
|
+
"add features while fixing builds"
|
|
8304
|
+
],
|
|
8305
|
+
escalationConditions: [
|
|
8306
|
+
"architectural problem causing build failure",
|
|
8307
|
+
"security issue discovered"
|
|
8308
|
+
],
|
|
8309
|
+
stopConditions: ["build passes", "escalation required"],
|
|
8310
|
+
successCriteria: [
|
|
8311
|
+
"build and type check pass",
|
|
8312
|
+
"minimum changes applied",
|
|
8313
|
+
"no type-safety suppression without explanation"
|
|
8314
|
+
]
|
|
8315
|
+
},
|
|
8316
|
+
{
|
|
8317
|
+
agent: "task-splitter",
|
|
8318
|
+
role: "Decompose complex tasks into parallel workstreams with explicit dependencies.",
|
|
8319
|
+
allowedTaskTypes: ["task-decomposition", "parallel-planning"],
|
|
8320
|
+
requiredInputs: ["complex task description"],
|
|
8321
|
+
expectedOutputFields: ["waves", "dependencies", "agent_assignments"],
|
|
8322
|
+
allowedTools: ["read", "glob", "grep", "planning-state"],
|
|
8323
|
+
forbiddenActions: [
|
|
8324
|
+
"execute tasks",
|
|
8325
|
+
"modify files",
|
|
8326
|
+
"assign non-existent agents"
|
|
8327
|
+
],
|
|
8328
|
+
escalationConditions: ["task too small to benefit from parallelization"],
|
|
8329
|
+
stopConditions: ["parallel execution plan complete"],
|
|
8330
|
+
successCriteria: [
|
|
8331
|
+
"plan delivered",
|
|
8332
|
+
"dependencies clear",
|
|
8333
|
+
"no file modifications"
|
|
8334
|
+
]
|
|
8335
|
+
},
|
|
8336
|
+
{
|
|
8337
|
+
agent: "discusser",
|
|
8338
|
+
role: "Extract requirements via structured Q&A and record decisions with D-XX numbering.",
|
|
8339
|
+
allowedTaskTypes: ["requirements-gathering", "discussion", "decision-recording"],
|
|
8340
|
+
requiredInputs: ["task description"],
|
|
8341
|
+
expectedOutputFields: ["decisions", "suppressed_questions", "open_questions"],
|
|
8342
|
+
allowedTools: ["read", "write", "edit", "glob", "grep", "planning-state"],
|
|
8343
|
+
forbiddenActions: [
|
|
8344
|
+
"implement features",
|
|
8345
|
+
"skip decision recording",
|
|
8346
|
+
"ask more than one question per turn"
|
|
8347
|
+
],
|
|
8348
|
+
escalationConditions: ["conflict with prior decisions"],
|
|
8349
|
+
stopConditions: ["requirements complete", "plan ready"],
|
|
8350
|
+
successCriteria: [
|
|
8351
|
+
"decisions recorded",
|
|
8352
|
+
"no open questions remain"
|
|
8353
|
+
]
|
|
8354
|
+
},
|
|
8355
|
+
{
|
|
8356
|
+
agent: "risk-analyst",
|
|
8357
|
+
role: "Assess risk of proposed changes using patch trust and regression signals. Read-only.",
|
|
8358
|
+
allowedTaskTypes: ["risk-assessment", "change-impact-analysis"],
|
|
8359
|
+
requiredInputs: ["change description", "trust signals"],
|
|
8360
|
+
expectedOutputFields: ["risk_level", "approval_required", "safer_alternative"],
|
|
8361
|
+
allowedTools: ["read", "glob", "grep"],
|
|
8362
|
+
forbiddenActions: [
|
|
8363
|
+
"modify files",
|
|
8364
|
+
"approve changes directly",
|
|
8365
|
+
"invent risk signals not in input data"
|
|
8366
|
+
],
|
|
8367
|
+
escalationConditions: ["critical risk identified"],
|
|
8368
|
+
stopConditions: ["risk report complete"],
|
|
8369
|
+
successCriteria: [
|
|
8370
|
+
"structured report delivered",
|
|
8371
|
+
"no file modifications"
|
|
8372
|
+
]
|
|
8373
|
+
},
|
|
8374
|
+
{
|
|
8375
|
+
agent: "policy-enforcer",
|
|
8376
|
+
role: "Apply policy gate matrix to decide whether a proposed edit can proceed. Read-only decision.",
|
|
8377
|
+
allowedTaskTypes: ["policy-enforcement", "gate-decision"],
|
|
8378
|
+
requiredInputs: ["file_path", "change_description", "policy_violations", "risk_score"],
|
|
8379
|
+
expectedOutputFields: ["decision", "trigger", "recommended_action"],
|
|
8380
|
+
allowedTools: ["read", "glob", "grep", "policy-engine"],
|
|
8381
|
+
forbiddenActions: [
|
|
8382
|
+
"modify files",
|
|
8383
|
+
"deviate from gate matrix",
|
|
8384
|
+
"approve blocked changes"
|
|
8385
|
+
],
|
|
8386
|
+
escalationConditions: ["arch constraint violation"],
|
|
8387
|
+
stopConditions: ["gate decision issued"],
|
|
8388
|
+
successCriteria: [
|
|
8389
|
+
"decision applied per matrix",
|
|
8390
|
+
"no file modifications"
|
|
8391
|
+
]
|
|
8392
|
+
},
|
|
8393
|
+
{
|
|
8394
|
+
agent: "performance-optimizer",
|
|
8395
|
+
role: "Identify and fix performance bottlenecks using profiling data and measurements.",
|
|
8396
|
+
allowedTaskTypes: ["performance-optimization", "profiling", "bottleneck-analysis"],
|
|
8397
|
+
requiredInputs: ["performance complaint or metric"],
|
|
8398
|
+
expectedOutputFields: ["baseline", "bottleneck", "fix", "after_measurement"],
|
|
8399
|
+
allowedTools: ["read", "write", "edit", "bash", "glob", "grep"],
|
|
8400
|
+
forbiddenActions: [
|
|
8401
|
+
"optimize without measurements",
|
|
8402
|
+
"refactor unrelated code",
|
|
8403
|
+
"skip before/after verification"
|
|
7602
8404
|
],
|
|
7603
|
-
escalationConditions: ["
|
|
7604
|
-
stopConditions: ["
|
|
7605
|
-
successCriteria: [
|
|
8405
|
+
escalationConditions: ["architectural bottleneck requiring redesign"],
|
|
8406
|
+
stopConditions: ["performance report complete"],
|
|
8407
|
+
successCriteria: [
|
|
8408
|
+
"before and after measurements provided",
|
|
8409
|
+
"improvement verified"
|
|
8410
|
+
]
|
|
7606
8411
|
},
|
|
7607
8412
|
{
|
|
7608
|
-
agent: "
|
|
7609
|
-
role: "
|
|
7610
|
-
allowedTaskTypes: ["
|
|
7611
|
-
requiredInputs: ["target
|
|
7612
|
-
expectedOutputFields: ["
|
|
7613
|
-
allowedTools: ["read", "
|
|
8413
|
+
agent: "refactor-guide",
|
|
8414
|
+
role: "Guide safe refactoring without changing behavior.",
|
|
8415
|
+
allowedTaskTypes: ["refactoring", "code-cleanup", "structure-improvement"],
|
|
8416
|
+
requiredInputs: ["refactoring target"],
|
|
8417
|
+
expectedOutputFields: ["transformations", "test_results"],
|
|
8418
|
+
allowedTools: ["read", "write", "edit", "bash", "glob", "grep"],
|
|
7614
8419
|
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"
|
|
8420
|
+
"add features",
|
|
8421
|
+
"leave tests failing",
|
|
8422
|
+
"combine multiple transformations in one step"
|
|
7625
8423
|
],
|
|
7626
|
-
escalationConditions: [
|
|
7627
|
-
|
|
7628
|
-
|
|
7629
|
-
"
|
|
8424
|
+
escalationConditions: ["tests fail during refactor"],
|
|
8425
|
+
stopConditions: ["refactor complete", "tests green"],
|
|
8426
|
+
successCriteria: [
|
|
8427
|
+
"behavior preserved",
|
|
8428
|
+
"tests pass"
|
|
8429
|
+
]
|
|
8430
|
+
},
|
|
8431
|
+
{
|
|
8432
|
+
agent: "auto-learner",
|
|
8433
|
+
role: "Capture reusable knowledge from session artifacts as skills.",
|
|
8434
|
+
allowedTaskTypes: ["knowledge-capture", "skill-creation"],
|
|
8435
|
+
requiredInputs: ["session reflection artifacts"],
|
|
8436
|
+
expectedOutputFields: ["skills_created", "summary"],
|
|
8437
|
+
allowedTools: ["read", "reflect", "write"],
|
|
8438
|
+
forbiddenActions: [
|
|
8439
|
+
"ask user questions",
|
|
8440
|
+
"create routine skills",
|
|
8441
|
+
"create more than 3 skills per session"
|
|
7630
8442
|
],
|
|
7631
|
-
|
|
8443
|
+
escalationConditions: ["no valuable patterns found"],
|
|
8444
|
+
stopConditions: ["skills written or no new skills identified"],
|
|
7632
8445
|
successCriteria: [
|
|
7633
|
-
"
|
|
7634
|
-
"
|
|
7635
|
-
"existing registry not modified",
|
|
7636
|
-
"decision is one of: approve, revise, block, escalate"
|
|
8446
|
+
"valuable patterns captured",
|
|
8447
|
+
"maximum 3 skills per session"
|
|
7637
8448
|
]
|
|
7638
8449
|
}
|
|
7639
8450
|
];
|
|
@@ -7641,6 +8452,9 @@ var REGISTRY = new Map(CONTRACTS.map((c) => [c.agent, c]));
|
|
|
7641
8452
|
function getContract(agent) {
|
|
7642
8453
|
return REGISTRY.get(agent) ?? null;
|
|
7643
8454
|
}
|
|
8455
|
+
function getAllContracts() {
|
|
8456
|
+
return [...CONTRACTS];
|
|
8457
|
+
}
|
|
7644
8458
|
|
|
7645
8459
|
// src/services/agent-validator.ts
|
|
7646
8460
|
function resolveValidatorMode(directory) {
|
|
@@ -8160,1622 +8974,1110 @@ function isPipedToShell(normalized) {
|
|
|
8160
8974
|
return true;
|
|
8161
8975
|
}
|
|
8162
8976
|
return false;
|
|
8163
|
-
}
|
|
8164
|
-
function isBashCommandDangerous(cmd) {
|
|
8165
|
-
const normalized = normalizeBashCommand(cmd);
|
|
8166
|
-
for (const p of BLOCKED_PATTERNS.bash) {
|
|
8167
|
-
if (normalized.includes(p)) {
|
|
8168
|
-
return `FLOWDECK: Dangerous bash command blocked (matched "${p}").`;
|
|
8169
|
-
}
|
|
8170
|
-
}
|
|
8171
|
-
if (isPipedToShell(normalized)) {
|
|
8172
|
-
return "FLOWDECK: Piping curl/wget directly to a shell is blocked.";
|
|
8173
|
-
}
|
|
8174
|
-
if (/\bdd\b/.test(normalized) && /\/dev\/(sd|hd|nvme|mmcblk|disk)/.test(normalized)) {
|
|
8175
|
-
return "FLOWDECK: dd command targeting a block device is blocked.";
|
|
8176
|
-
}
|
|
8177
|
-
if (/:\s*\(\s*\)\s*\{\s*:\s*\|\s*:\s*&\s*}\s*;\s*:/.test(normalized)) {
|
|
8178
|
-
return "FLOWDECK: Fork bomb pattern is blocked.";
|
|
8179
|
-
}
|
|
8180
|
-
return null;
|
|
8181
|
-
}
|
|
8182
|
-
function isBlocked(tool13, args) {
|
|
8183
|
-
const patterns = BLOCKED_PATTERNS[tool13];
|
|
8184
|
-
if (!patterns)
|
|
8185
|
-
return null;
|
|
8186
|
-
if (tool13 === "bash") {
|
|
8187
|
-
const cmd = args.command;
|
|
8188
|
-
if (!cmd)
|
|
8189
|
-
return null;
|
|
8190
|
-
return isBashCommandDangerous(cmd);
|
|
8191
|
-
}
|
|
8192
|
-
if (tool13 === "read" || tool13 === "write") {
|
|
8193
|
-
const filePath = extractTargetPath(args);
|
|
8194
|
-
if (!filePath)
|
|
8195
|
-
return null;
|
|
8196
|
-
for (const p of patterns) {
|
|
8197
|
-
if (filePath.includes(p)) {
|
|
8198
|
-
return tool13 === "read" ? `FLOWDECK: Access to "${p}" files is blocked.` : `FLOWDECK: Writing to "${p}" is blocked.`;
|
|
8199
|
-
}
|
|
8200
|
-
}
|
|
8201
|
-
return null;
|
|
8202
|
-
}
|
|
8203
|
-
return null;
|
|
8204
|
-
}
|
|
8205
|
-
function checkArchConstraint(directory, filePath) {
|
|
8206
|
-
const constraintsPath = join20(codebaseDir(directory), "CONSTRAINTS.md");
|
|
8207
|
-
if (!existsSync20(constraintsPath))
|
|
8208
|
-
return null;
|
|
8209
|
-
try {
|
|
8210
|
-
const content = readFileSync20(constraintsPath, "utf-8");
|
|
8211
|
-
const match = content.match(/## Forbidden Paths\n([\s\S]*?)(?:\n##|$)/);
|
|
8212
|
-
if (!match)
|
|
8213
|
-
return null;
|
|
8214
|
-
for (const line of match[1].split(`
|
|
8215
|
-
`)) {
|
|
8216
|
-
const pattern = line.replace(/^-\s*/, "").split("#")[0].trim();
|
|
8217
|
-
if (pattern && filePath.includes(pattern)) {
|
|
8218
|
-
return `FLOWDECK [arch-constraint]: editing "${pattern}" is forbidden by .codebase/CONSTRAINTS.md`;
|
|
8219
|
-
}
|
|
8220
|
-
}
|
|
8221
|
-
} catch {}
|
|
8222
|
-
return null;
|
|
8223
|
-
}
|
|
8224
|
-
function checkPhaseEnforcement(directory) {
|
|
8225
|
-
try {
|
|
8226
|
-
const state = readPlanningState(directory);
|
|
8227
|
-
const flowdeckConfig = resolveDesignFirstConfig(loadFlowDeckConfig(directory));
|
|
8228
|
-
if (state.phase > 0 && state.phase < 3) {
|
|
8229
|
-
return `FLOWDECK [phase-gate]: writing to codebase is blocked in phase ${state.phase} (${state.phase === 1 ? "discuss" : "plan"}). Run /fd-plan --confirm to enter execute phase.`;
|
|
8230
|
-
}
|
|
8231
|
-
if (flowdeckConfig.enabled && flowdeckConfig.requireApprovalBeforeImplementation && isUiDesignApprovalRequired(directory)) {
|
|
8232
|
-
if (flowdeckConfig.enforcement === "advisory") {
|
|
8233
|
-
return `FLOWDECK [design-gate]: advisory design-first mode detected missing approval. Run /fd-design --mode=draft or set design_override=true in STATE.md.`;
|
|
8234
|
-
}
|
|
8235
|
-
return `FLOWDECK [design-gate]: UI-heavy task requires approved design handoff before implementation. Run /fd-design --mode=draft and ensure design_stage=handoff_complete + design_approved=true, or set explicit design_override with reason.`;
|
|
8236
|
-
}
|
|
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
|
-
}
|
|
8247
|
-
if (state.task_type && isUiHeavyTask(state.task_type)) {
|
|
8248
|
-
return !(state.design_stage === "handoff_complete" && state.design_approved);
|
|
8249
|
-
}
|
|
8250
|
-
const planPath = phasePlanPath(directory, state.phase || 1);
|
|
8251
|
-
if (!existsSync20(planPath))
|
|
8252
|
-
return false;
|
|
8253
|
-
const planContent = readFileSync20(planPath, "utf-8");
|
|
8254
|
-
if (!isUiHeavyTask(planContent))
|
|
8255
|
-
return false;
|
|
8256
|
-
return !(state.design_stage === "handoff_complete" && state.design_approved);
|
|
8257
|
-
}
|
|
8258
|
-
function allow(reason, riskFlags = []) {
|
|
8259
|
-
return { verdict: "allow", reason, riskFlags, source: "tool-guard" };
|
|
8260
|
-
}
|
|
8261
|
-
function deny(reason, escalationMessage, riskFlags = []) {
|
|
8262
|
-
return {
|
|
8263
|
-
verdict: "deny",
|
|
8264
|
-
reason,
|
|
8265
|
-
riskFlags,
|
|
8266
|
-
source: "tool-guard",
|
|
8267
|
-
escalationMessage
|
|
8268
|
-
};
|
|
8269
|
-
}
|
|
8270
|
-
function evaluate(input) {
|
|
8271
|
-
const { directory, tool: tool13, args } = input;
|
|
8272
|
-
if (tool13 !== "bash" && tool13 !== "read" && tool13 !== "write" && tool13 !== "edit") {
|
|
8273
|
-
return allow("Tool is not guardable");
|
|
8274
|
-
}
|
|
8275
|
-
const blocked = isBlocked(tool13, args);
|
|
8276
|
-
if (blocked) {
|
|
8277
|
-
return deny(blocked, blocked, ["dangerous-pattern"]);
|
|
8278
|
-
}
|
|
8279
|
-
if (tool13 === "write" || tool13 === "edit") {
|
|
8280
|
-
const phaseBlock = checkPhaseEnforcement(directory);
|
|
8281
|
-
if (phaseBlock) {
|
|
8282
|
-
const isAdvisory = phaseBlock.includes("[design-gate]: advisory");
|
|
8283
|
-
if (isAdvisory) {
|
|
8284
|
-
return allow(phaseBlock, ["design-gate-advisory"]);
|
|
8285
|
-
}
|
|
8286
|
-
return deny(phaseBlock, phaseBlock, ["phase-gate"]);
|
|
8287
|
-
}
|
|
8288
|
-
const filePath = extractTargetPath(args);
|
|
8289
|
-
const constraintBlock = checkArchConstraint(directory, filePath);
|
|
8290
|
-
if (constraintBlock) {
|
|
8291
|
-
return deny(constraintBlock, constraintBlock, ["arch-constraint"]);
|
|
8292
|
-
}
|
|
8293
|
-
}
|
|
8294
|
-
return allow("Tool guard passed");
|
|
8295
|
-
}
|
|
8296
|
-
|
|
8297
|
-
// src/hooks/guard-rails.ts
|
|
8298
|
-
import { existsSync as existsSync21, readFileSync as readFileSync21 } from "fs";
|
|
8299
|
-
import { join as join21 } from "path";
|
|
8300
|
-
var PLANNING_DIR2 = ".planning";
|
|
8301
|
-
var CONFIG_FILE = "config.json";
|
|
8302
|
-
var STATE_FILE2 = "STATE.md";
|
|
8303
|
-
function resolveExecutionMode(configPath, trustScore, volatility) {
|
|
8304
|
-
if (existsSync21(configPath)) {
|
|
8305
|
-
try {
|
|
8306
|
-
const config = JSON.parse(readFileSync21(configPath, "utf-8"));
|
|
8307
|
-
if (config.execution_mode === "review-only")
|
|
8308
|
-
return "review-only";
|
|
8309
|
-
if (config.execution_mode === "guarded")
|
|
8310
|
-
return "guarded";
|
|
8311
|
-
if (config.execution_mode === "auto")
|
|
8312
|
-
return "auto";
|
|
8313
|
-
} catch {}
|
|
8314
|
-
}
|
|
8315
|
-
if (trustScore !== null) {
|
|
8316
|
-
if (trustScore < 30)
|
|
8317
|
-
return "review-only";
|
|
8318
|
-
if (trustScore < 60)
|
|
8319
|
-
return "guarded";
|
|
8320
|
-
}
|
|
8321
|
-
if (volatility === "critical")
|
|
8322
|
-
return "review-only";
|
|
8323
|
-
if (volatility === "volatile")
|
|
8324
|
-
return "guarded";
|
|
8325
|
-
return "auto";
|
|
8326
|
-
}
|
|
8327
|
-
var BUILD_DEPLOY_PATTERNS = [
|
|
8328
|
-
"npm build",
|
|
8329
|
-
"npm run build",
|
|
8330
|
-
"bun build",
|
|
8331
|
-
"yarn build",
|
|
8332
|
-
"npm deploy",
|
|
8333
|
-
"yarn deploy",
|
|
8334
|
-
"bun deploy",
|
|
8335
|
-
"npm install",
|
|
8336
|
-
"yarn install",
|
|
8337
|
-
"bun install",
|
|
8338
|
-
"make build",
|
|
8339
|
-
"make deploy",
|
|
8340
|
-
"docker build",
|
|
8341
|
-
"docker push",
|
|
8342
|
-
"docker-compose",
|
|
8343
|
-
"git push",
|
|
8344
|
-
"git deploy",
|
|
8345
|
-
"gradle build",
|
|
8346
|
-
"mvn package",
|
|
8347
|
-
"ant build",
|
|
8348
|
-
"cargo build",
|
|
8349
|
-
"cargo deploy",
|
|
8350
|
-
"python setup.py",
|
|
8351
|
-
"pip install",
|
|
8352
|
-
"rails deploy",
|
|
8353
|
-
"rake deploy"
|
|
8354
|
-
];
|
|
8355
|
-
function allow2(reason, riskFlags = []) {
|
|
8356
|
-
return { verdict: "allow", reason, riskFlags, source: "guard-rails" };
|
|
8357
|
-
}
|
|
8358
|
-
function deny2(reason, escalationMessage, riskFlags = []) {
|
|
8359
|
-
return {
|
|
8360
|
-
verdict: "deny",
|
|
8361
|
-
reason,
|
|
8362
|
-
riskFlags,
|
|
8363
|
-
source: "guard-rails",
|
|
8364
|
-
escalationMessage
|
|
8365
|
-
};
|
|
8366
|
-
}
|
|
8367
|
-
function evaluate2(input) {
|
|
8368
|
-
const { directory, tool: tool13, args } = input;
|
|
8369
|
-
const planningDirPath = join21(directory, PLANNING_DIR2);
|
|
8370
|
-
const codebaseDirectory = codebaseDir(directory);
|
|
8371
|
-
const configPath = join21(planningDirPath, CONFIG_FILE);
|
|
8372
|
-
const statePath3 = join21(planningDirPath, STATE_FILE2);
|
|
8373
|
-
const workspaceRoot = findWorkspaceRoot(directory);
|
|
8374
|
-
if (workspaceRoot && directory !== workspaceRoot) {
|
|
8375
|
-
const workspaceConfig = getWorkspaceConfig(directory);
|
|
8376
|
-
if (workspaceConfig && workspaceConfig.workspace_mode === "shared" && !existsSync21(planningDirPath)) {
|
|
8377
|
-
const msg = `No .planning/ in this sub-repo. Switch to workspace root: cd ${workspaceRoot}`;
|
|
8378
|
-
return deny2(msg, `[flowdeck] BLOCK: ${msg}`, ["workspace-shared-mode"]);
|
|
8977
|
+
}
|
|
8978
|
+
function isBashCommandDangerous(cmd) {
|
|
8979
|
+
const normalized = normalizeBashCommand(cmd);
|
|
8980
|
+
for (const p of BLOCKED_PATTERNS.bash) {
|
|
8981
|
+
if (normalized.includes(p)) {
|
|
8982
|
+
return `FLOWDECK: Dangerous bash command blocked (matched "${p}").`;
|
|
8379
8983
|
}
|
|
8380
8984
|
}
|
|
8381
|
-
if (
|
|
8382
|
-
|
|
8383
|
-
return allow2("FlowDeck not initialized in this directory — skipping guard-rails");
|
|
8384
|
-
}
|
|
8385
|
-
if (!existsSync21(codebaseDirectory)) {
|
|
8386
|
-
const msg = ".codebase/ not found. Run /fd-map-codebase to map the codebase.";
|
|
8387
|
-
return allow2(msg, ["codebase-missing"]);
|
|
8388
|
-
}
|
|
8389
|
-
const execMode = resolveExecutionMode(configPath, null);
|
|
8390
|
-
if (execMode === "review-only") {
|
|
8391
|
-
const msg = "review-only mode: propose diff but do not apply. Set execution_mode in .planning/config.json to change.";
|
|
8392
|
-
return deny2(msg, `[flowdeck] BLOCK (${msg})`, ["review-only-mode"]);
|
|
8393
|
-
}
|
|
8394
|
-
if (execMode === "guarded") {
|
|
8395
|
-
return allow2("guarded mode: edit will proceed but flag for human review", ["guarded-mode"]);
|
|
8396
|
-
}
|
|
8397
|
-
const designGateMessage = getDesignGateMessage(directory);
|
|
8398
|
-
if (designGateMessage) {
|
|
8399
|
-
if (designGateMessage.startsWith("[flowdeck] WARNING:")) {
|
|
8400
|
-
return allow2(designGateMessage, ["design-gate-advisory"]);
|
|
8401
|
-
}
|
|
8402
|
-
return deny2(designGateMessage, designGateMessage, ["design-gate"]);
|
|
8403
|
-
}
|
|
8404
|
-
const severity = effectiveSeverity(configPath, statePath3);
|
|
8405
|
-
if (severity === null) {
|
|
8406
|
-
return allow2("guard_enforcement is off");
|
|
8407
|
-
}
|
|
8408
|
-
if (severity === "warn") {
|
|
8409
|
-
const warning = getWarningMessage(planningDirPath);
|
|
8410
|
-
return allow2(`[flowdeck] WARNING: ${warning}`, ["plan-not-confirmed"]);
|
|
8411
|
-
}
|
|
8412
|
-
const blockMessage = getBlockMessage(planningDirPath);
|
|
8413
|
-
return deny2(blockMessage, `[flowdeck] BLOCK: ${blockMessage}`, ["plan-not-confirmed"]);
|
|
8985
|
+
if (isPipedToShell(normalized)) {
|
|
8986
|
+
return "FLOWDECK: Piping curl/wget directly to a shell is blocked.";
|
|
8414
8987
|
}
|
|
8415
|
-
if (
|
|
8416
|
-
|
|
8417
|
-
for (const pattern of BUILD_DEPLOY_PATTERNS) {
|
|
8418
|
-
if (cmd.includes(pattern)) {
|
|
8419
|
-
if (!getPlanConfirmed(statePath3)) {
|
|
8420
|
-
const msg = "Build/deploy command detected but plan is not confirmed. Run /fd-plan first.";
|
|
8421
|
-
return allow2(`[flowdeck] WARNING: ${msg}`, ["build-deploy-without-plan"]);
|
|
8422
|
-
}
|
|
8423
|
-
break;
|
|
8424
|
-
}
|
|
8425
|
-
}
|
|
8988
|
+
if (/\bdd\b/.test(normalized) && /\/dev\/(sd|hd|nvme|mmcblk|disk)/.test(normalized)) {
|
|
8989
|
+
return "FLOWDECK: dd command targeting a block device is blocked.";
|
|
8426
8990
|
}
|
|
8427
|
-
|
|
8991
|
+
if (/:\s*\(\s*\)\s*\{\s*:\s*\|\s*:\s*&\s*}\s*;\s*:/.test(normalized)) {
|
|
8992
|
+
return "FLOWDECK: Fork bomb pattern is blocked.";
|
|
8993
|
+
}
|
|
8994
|
+
return null;
|
|
8428
8995
|
}
|
|
8429
|
-
function
|
|
8430
|
-
const
|
|
8431
|
-
if (!
|
|
8432
|
-
return null;
|
|
8433
|
-
const state = readPlanningState(dir);
|
|
8434
|
-
if (state.design_override && state.design_override_reason && state.design_override_reason.trim().length > 0)
|
|
8996
|
+
function isBlocked(tool13, args) {
|
|
8997
|
+
const patterns = BLOCKED_PATTERNS[tool13];
|
|
8998
|
+
if (!patterns)
|
|
8435
8999
|
return null;
|
|
8436
|
-
|
|
8437
|
-
|
|
8438
|
-
if (
|
|
9000
|
+
if (tool13 === "bash") {
|
|
9001
|
+
const cmd = args.command;
|
|
9002
|
+
if (!cmd)
|
|
8439
9003
|
return null;
|
|
8440
|
-
|
|
8441
|
-
|
|
9004
|
+
return isBashCommandDangerous(cmd);
|
|
9005
|
+
}
|
|
9006
|
+
if (tool13 === "read" || tool13 === "write") {
|
|
9007
|
+
const filePath = extractTargetPath(args);
|
|
9008
|
+
if (!filePath)
|
|
9009
|
+
return null;
|
|
9010
|
+
for (const p of patterns) {
|
|
9011
|
+
if (filePath.includes(p)) {
|
|
9012
|
+
return tool13 === "read" ? `FLOWDECK: Access to "${p}" files is blocked.` : `FLOWDECK: Writing to "${p}" is blocked.`;
|
|
9013
|
+
}
|
|
8442
9014
|
}
|
|
8443
|
-
return
|
|
9015
|
+
return null;
|
|
8444
9016
|
}
|
|
8445
9017
|
return null;
|
|
8446
9018
|
}
|
|
8447
|
-
function
|
|
8448
|
-
const
|
|
8449
|
-
if (!
|
|
8450
|
-
return
|
|
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 {}
|
|
8466
|
-
}
|
|
8467
|
-
return getPlanConfirmed(statePath3) ? "block" : "warn";
|
|
8468
|
-
}
|
|
8469
|
-
function getPlanConfirmed(statePath3) {
|
|
8470
|
-
if (!existsSync21(statePath3))
|
|
8471
|
-
return false;
|
|
9019
|
+
function checkArchConstraint(directory, filePath) {
|
|
9020
|
+
const constraintsPath = join20(codebaseDir(directory), "CONSTRAINTS.md");
|
|
9021
|
+
if (!existsSync20(constraintsPath))
|
|
9022
|
+
return null;
|
|
8472
9023
|
try {
|
|
8473
|
-
const content =
|
|
8474
|
-
const match = content.match(
|
|
8475
|
-
|
|
8476
|
-
|
|
8477
|
-
|
|
8478
|
-
|
|
8479
|
-
|
|
8480
|
-
|
|
8481
|
-
|
|
8482
|
-
|
|
8483
|
-
|
|
8484
|
-
|
|
8485
|
-
|
|
8486
|
-
function getBlockMessage(planningDir2) {
|
|
8487
|
-
if (!existsSync21(join21(planningDir2, STATE_FILE2))) {
|
|
8488
|
-
return "No STATE.md found. Run /fd-map-codebase then /fd-new-feature to start a feature.";
|
|
8489
|
-
}
|
|
8490
|
-
return "Plan not confirmed. Run /fd-plan and confirm to enable execution.";
|
|
8491
|
-
}
|
|
8492
|
-
|
|
8493
|
-
// src/services/approval-manager.ts
|
|
8494
|
-
import { existsSync as existsSync22, readFileSync as readFileSync22, writeFileSync as writeFileSync13, mkdirSync as mkdirSync12 } from "fs";
|
|
8495
|
-
import { join as join22 } from "path";
|
|
8496
|
-
import { createHash as createHash4 } from "crypto";
|
|
8497
|
-
import { randomUUID } from "crypto";
|
|
8498
|
-
var APPROVAL_TTL_MS = 30 * 60 * 1000;
|
|
8499
|
-
var SENSITIVE_PATTERNS = [
|
|
8500
|
-
/auth/i,
|
|
8501
|
-
/login/i,
|
|
8502
|
-
/password/i,
|
|
8503
|
-
/secret/i,
|
|
8504
|
-
/token/i,
|
|
8505
|
-
/jwt/i,
|
|
8506
|
-
/session/i,
|
|
8507
|
-
/oauth/i,
|
|
8508
|
-
/payment/i,
|
|
8509
|
-
/billing/i,
|
|
8510
|
-
/stripe/i,
|
|
8511
|
-
/credit/i,
|
|
8512
|
-
/migration/i,
|
|
8513
|
-
/migrate/i,
|
|
8514
|
-
/schema/i,
|
|
8515
|
-
/alembic/i,
|
|
8516
|
-
/infra/i,
|
|
8517
|
-
/terraform/i,
|
|
8518
|
-
/ansible/i,
|
|
8519
|
-
/k8s/i,
|
|
8520
|
-
/kubernetes/i,
|
|
8521
|
-
/docker/i,
|
|
8522
|
-
/\.env/i,
|
|
8523
|
-
/secrets\./i,
|
|
8524
|
-
/config\/prod/i,
|
|
8525
|
-
/production/i,
|
|
8526
|
-
/admin/i,
|
|
8527
|
-
/privilege/i,
|
|
8528
|
-
/sudo/i
|
|
8529
|
-
];
|
|
8530
|
-
function isSensitivePath(filePath) {
|
|
8531
|
-
return SENSITIVE_PATTERNS.some((p) => p.test(filePath));
|
|
8532
|
-
}
|
|
8533
|
-
function approvalsPath(dir) {
|
|
8534
|
-
return join22(codebaseDir(dir), "APPROVALS.json");
|
|
9024
|
+
const content = readFileSync20(constraintsPath, "utf-8");
|
|
9025
|
+
const match = content.match(/## Forbidden Paths\n([\s\S]*?)(?:\n##|$)/);
|
|
9026
|
+
if (!match)
|
|
9027
|
+
return null;
|
|
9028
|
+
for (const line of match[1].split(`
|
|
9029
|
+
`)) {
|
|
9030
|
+
const pattern = line.replace(/^-\s*/, "").split("#")[0].trim();
|
|
9031
|
+
if (pattern && filePath.includes(pattern)) {
|
|
9032
|
+
return `FLOWDECK [arch-constraint]: editing "${pattern}" is forbidden by .codebase/CONSTRAINTS.md`;
|
|
9033
|
+
}
|
|
9034
|
+
}
|
|
9035
|
+
} catch {}
|
|
9036
|
+
return null;
|
|
8535
9037
|
}
|
|
8536
|
-
function
|
|
8537
|
-
const p = approvalsPath(dir);
|
|
8538
|
-
if (!existsSync22(p))
|
|
8539
|
-
return { requests: [] };
|
|
9038
|
+
function checkPhaseEnforcement(directory) {
|
|
8540
9039
|
try {
|
|
8541
|
-
|
|
8542
|
-
|
|
8543
|
-
|
|
8544
|
-
|
|
8545
|
-
}
|
|
8546
|
-
|
|
8547
|
-
|
|
8548
|
-
|
|
8549
|
-
|
|
8550
|
-
|
|
8551
|
-
}
|
|
8552
|
-
|
|
8553
|
-
|
|
8554
|
-
const req = {
|
|
8555
|
-
id: randomUUID(),
|
|
8556
|
-
run_id,
|
|
8557
|
-
session_id: options.session_id ?? "session-0",
|
|
8558
|
-
requested_at: new Date().toISOString(),
|
|
8559
|
-
status: "pending",
|
|
8560
|
-
trigger,
|
|
8561
|
-
reason,
|
|
8562
|
-
risk_score: options.risk_score ?? 0,
|
|
8563
|
-
...options.agent ? { agent: options.agent } : {},
|
|
8564
|
-
...options.file_path ? { file_path: options.file_path } : {},
|
|
8565
|
-
...options.content_hash ? { content_hash: options.content_hash } : {},
|
|
8566
|
-
...options.change_description ? { change_description: options.change_description } : {}
|
|
8567
|
-
};
|
|
8568
|
-
store.requests.push(req);
|
|
8569
|
-
saveStore(dir, store);
|
|
8570
|
-
return req;
|
|
8571
|
-
}
|
|
8572
|
-
function checkApproval(dir, criteria) {
|
|
8573
|
-
const store = loadStore(dir);
|
|
8574
|
-
const now = Date.now();
|
|
8575
|
-
return store.requests.filter((r) => r.status === "approved" && r.resolved_at && r.run_id === criteria.run_id && r.session_id === criteria.session_id && r.agent === criteria.agent && r.file_path === criteria.file_path && r.content_hash === criteria.content_hash && now - new Date(r.resolved_at).getTime() < APPROVAL_TTL_MS).sort((a, b) => b.resolved_at.localeCompare(a.resolved_at)).at(0) ?? null;
|
|
8576
|
-
}
|
|
8577
|
-
function computeContentHash(args) {
|
|
8578
|
-
const keys = Object.keys(args).sort();
|
|
8579
|
-
const payload = JSON.stringify(args, keys);
|
|
8580
|
-
return createHash4("sha256").update(payload).digest("hex").slice(0, 16);
|
|
8581
|
-
}
|
|
8582
|
-
function requestApprovalForTool(dir, run_id, session_id, agent, tool13, args) {
|
|
8583
|
-
const filePath = extractTargetPath2(args);
|
|
8584
|
-
const isSensitive = filePath ? isSensitivePath(filePath) : false;
|
|
8585
|
-
return requestApproval(dir, run_id, tool13, `Approval required for tool "${tool13}"`, {
|
|
8586
|
-
file_path: filePath,
|
|
8587
|
-
risk_score: isSensitive ? 30 : 50,
|
|
8588
|
-
session_id,
|
|
8589
|
-
agent,
|
|
8590
|
-
content_hash: computeContentHash(args),
|
|
8591
|
-
change_description: `Tool "${tool13}" requested on ${filePath || "unknown target"}`
|
|
8592
|
-
});
|
|
9040
|
+
const state = readPlanningState(directory);
|
|
9041
|
+
const flowdeckConfig = resolveDesignFirstConfig(loadFlowDeckConfig(directory));
|
|
9042
|
+
if (state.phase > 0 && state.phase < 3) {
|
|
9043
|
+
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.`;
|
|
9044
|
+
}
|
|
9045
|
+
if (flowdeckConfig.enabled && flowdeckConfig.requireApprovalBeforeImplementation && isUiDesignApprovalRequired(directory)) {
|
|
9046
|
+
if (flowdeckConfig.enforcement === "advisory") {
|
|
9047
|
+
return `FLOWDECK [design-gate]: advisory design-first mode detected missing approval. Run /fd-design --mode=draft or set design_override=true in STATE.md.`;
|
|
9048
|
+
}
|
|
9049
|
+
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.`;
|
|
9050
|
+
}
|
|
9051
|
+
} catch {}
|
|
9052
|
+
return null;
|
|
8593
9053
|
}
|
|
8594
|
-
function
|
|
8595
|
-
|
|
9054
|
+
function isUiDesignApprovalRequired(directory) {
|
|
9055
|
+
const state = readPlanningState(directory);
|
|
9056
|
+
if (state.design_override && state.design_override_reason && state.design_override_reason.trim().length > 0)
|
|
9057
|
+
return false;
|
|
9058
|
+
if (state.requires_design_first) {
|
|
9059
|
+
return !(state.design_stage === "handoff_complete" && state.design_approved);
|
|
9060
|
+
}
|
|
9061
|
+
if (state.task_type && isUiHeavyTask(state.task_type)) {
|
|
9062
|
+
return !(state.design_stage === "handoff_complete" && state.design_approved);
|
|
9063
|
+
}
|
|
9064
|
+
const planPath = phasePlanPath(directory, state.phase || 1);
|
|
9065
|
+
if (!existsSync20(planPath))
|
|
9066
|
+
return false;
|
|
9067
|
+
const planContent = readFileSync20(planPath, "utf-8");
|
|
9068
|
+
if (!isUiHeavyTask(planContent))
|
|
9069
|
+
return false;
|
|
9070
|
+
return !(state.design_stage === "handoff_complete" && state.design_approved);
|
|
8596
9071
|
}
|
|
8597
|
-
|
|
8598
|
-
|
|
8599
|
-
var WRITE_TOOLS = new Set([
|
|
8600
|
-
"write_file",
|
|
8601
|
-
"edit_file",
|
|
8602
|
-
"create_file",
|
|
8603
|
-
"apply_patch",
|
|
8604
|
-
"str_replace_editor",
|
|
8605
|
-
"write",
|
|
8606
|
-
"edit"
|
|
8607
|
-
]);
|
|
8608
|
-
function allow3(reason) {
|
|
8609
|
-
return { verdict: "allow", reason, riskFlags: [], source: "approval-manager" };
|
|
9072
|
+
function allow(reason, riskFlags = []) {
|
|
9073
|
+
return { verdict: "allow", reason, riskFlags, source: "tool-guard" };
|
|
8610
9074
|
}
|
|
8611
|
-
function
|
|
9075
|
+
function deny(reason, escalationMessage, riskFlags = []) {
|
|
8612
9076
|
return {
|
|
8613
|
-
verdict: "
|
|
9077
|
+
verdict: "deny",
|
|
8614
9078
|
reason,
|
|
8615
9079
|
riskFlags,
|
|
8616
|
-
source: "
|
|
9080
|
+
source: "tool-guard",
|
|
9081
|
+
escalationMessage
|
|
8617
9082
|
};
|
|
8618
9083
|
}
|
|
8619
|
-
function
|
|
9084
|
+
function evaluate(input) {
|
|
8620
9085
|
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");
|
|
9086
|
+
if (tool13 !== "bash" && tool13 !== "read" && tool13 !== "write" && tool13 !== "edit") {
|
|
9087
|
+
return allow("Tool is not guardable");
|
|
8627
9088
|
}
|
|
8628
|
-
|
|
8629
|
-
|
|
9089
|
+
const blocked = isBlocked(tool13, args);
|
|
9090
|
+
if (blocked) {
|
|
9091
|
+
return deny(blocked, blocked, ["dangerous-pattern"]);
|
|
8630
9092
|
}
|
|
8631
|
-
|
|
8632
|
-
|
|
8633
|
-
|
|
8634
|
-
|
|
8635
|
-
|
|
8636
|
-
|
|
8637
|
-
|
|
8638
|
-
|
|
8639
|
-
|
|
9093
|
+
if (tool13 === "write" || tool13 === "edit") {
|
|
9094
|
+
const phaseBlock = checkPhaseEnforcement(directory);
|
|
9095
|
+
if (phaseBlock) {
|
|
9096
|
+
const isAdvisory = phaseBlock.includes("[design-gate]: advisory");
|
|
9097
|
+
if (isAdvisory) {
|
|
9098
|
+
return allow(phaseBlock, ["design-gate-advisory"]);
|
|
9099
|
+
}
|
|
9100
|
+
return deny(phaseBlock, phaseBlock, ["phase-gate"]);
|
|
9101
|
+
}
|
|
9102
|
+
const filePath = extractTargetPath(args);
|
|
9103
|
+
const constraintBlock = checkArchConstraint(directory, filePath);
|
|
9104
|
+
if (constraintBlock) {
|
|
9105
|
+
return deny(constraintBlock, constraintBlock, ["arch-constraint"]);
|
|
9106
|
+
}
|
|
8640
9107
|
}
|
|
8641
|
-
return
|
|
9108
|
+
return allow("Tool guard passed");
|
|
8642
9109
|
}
|
|
8643
9110
|
|
|
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 [];
|
|
9111
|
+
// src/hooks/guard-rails.ts
|
|
9112
|
+
import { existsSync as existsSync21, readFileSync as readFileSync21 } from "fs";
|
|
9113
|
+
import { join as join21 } from "path";
|
|
9114
|
+
var PLANNING_DIR2 = ".planning";
|
|
9115
|
+
var CONFIG_FILE = "config.json";
|
|
9116
|
+
var STATE_FILE2 = "STATE.md";
|
|
9117
|
+
function resolveExecutionMode(configPath, trustScore, volatility) {
|
|
9118
|
+
if (existsSync21(configPath)) {
|
|
9119
|
+
try {
|
|
9120
|
+
const config = JSON.parse(readFileSync21(configPath, "utf-8"));
|
|
9121
|
+
if (config.execution_mode === "review-only")
|
|
9122
|
+
return "review-only";
|
|
9123
|
+
if (config.execution_mode === "guarded")
|
|
9124
|
+
return "guarded";
|
|
9125
|
+
if (config.execution_mode === "auto")
|
|
9126
|
+
return "auto";
|
|
9127
|
+
} catch {}
|
|
8665
9128
|
}
|
|
9129
|
+
if (trustScore !== null) {
|
|
9130
|
+
if (trustScore < 30)
|
|
9131
|
+
return "review-only";
|
|
9132
|
+
if (trustScore < 60)
|
|
9133
|
+
return "guarded";
|
|
9134
|
+
}
|
|
9135
|
+
if (volatility === "critical")
|
|
9136
|
+
return "review-only";
|
|
9137
|
+
if (volatility === "volatile")
|
|
9138
|
+
return "guarded";
|
|
9139
|
+
return "auto";
|
|
8666
9140
|
}
|
|
8667
|
-
|
|
8668
|
-
|
|
8669
|
-
|
|
8670
|
-
|
|
8671
|
-
|
|
8672
|
-
|
|
8673
|
-
|
|
8674
|
-
|
|
8675
|
-
|
|
8676
|
-
|
|
8677
|
-
|
|
8678
|
-
|
|
8679
|
-
|
|
8680
|
-
|
|
8681
|
-
|
|
8682
|
-
|
|
8683
|
-
|
|
8684
|
-
|
|
8685
|
-
|
|
8686
|
-
|
|
8687
|
-
|
|
8688
|
-
|
|
8689
|
-
|
|
8690
|
-
|
|
8691
|
-
|
|
8692
|
-
|
|
8693
|
-
|
|
8694
|
-
|
|
8695
|
-
|
|
8696
|
-
};
|
|
8697
|
-
appendFileSync4(agentSpansPath(dir), JSON.stringify(span) + `
|
|
8698
|
-
`, "utf-8");
|
|
8699
|
-
return span;
|
|
9141
|
+
var BUILD_DEPLOY_PATTERNS = [
|
|
9142
|
+
"npm build",
|
|
9143
|
+
"npm run build",
|
|
9144
|
+
"bun build",
|
|
9145
|
+
"yarn build",
|
|
9146
|
+
"npm deploy",
|
|
9147
|
+
"yarn deploy",
|
|
9148
|
+
"bun deploy",
|
|
9149
|
+
"npm install",
|
|
9150
|
+
"yarn install",
|
|
9151
|
+
"bun install",
|
|
9152
|
+
"make build",
|
|
9153
|
+
"make deploy",
|
|
9154
|
+
"docker build",
|
|
9155
|
+
"docker push",
|
|
9156
|
+
"docker-compose",
|
|
9157
|
+
"git push",
|
|
9158
|
+
"git deploy",
|
|
9159
|
+
"gradle build",
|
|
9160
|
+
"mvn package",
|
|
9161
|
+
"ant build",
|
|
9162
|
+
"cargo build",
|
|
9163
|
+
"cargo deploy",
|
|
9164
|
+
"python setup.py",
|
|
9165
|
+
"pip install",
|
|
9166
|
+
"rails deploy",
|
|
9167
|
+
"rake deploy"
|
|
9168
|
+
];
|
|
9169
|
+
function allow2(reason, riskFlags = []) {
|
|
9170
|
+
return { verdict: "allow", reason, riskFlags, source: "guard-rails" };
|
|
8700
9171
|
}
|
|
8701
|
-
function
|
|
8702
|
-
|
|
8703
|
-
|
|
8704
|
-
|
|
8705
|
-
|
|
8706
|
-
|
|
8707
|
-
|
|
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
|
|
9172
|
+
function deny2(reason, escalationMessage, riskFlags = []) {
|
|
9173
|
+
return {
|
|
9174
|
+
verdict: "deny",
|
|
9175
|
+
reason,
|
|
9176
|
+
riskFlags,
|
|
9177
|
+
source: "guard-rails",
|
|
9178
|
+
escalationMessage
|
|
8718
9179
|
};
|
|
8719
|
-
saveAllSpans(dir, spans);
|
|
8720
9180
|
}
|
|
8721
|
-
function
|
|
8722
|
-
const
|
|
8723
|
-
const
|
|
8724
|
-
|
|
8725
|
-
|
|
8726
|
-
|
|
8727
|
-
|
|
8728
|
-
|
|
9181
|
+
function evaluate2(input) {
|
|
9182
|
+
const { directory, tool: tool13, args } = input;
|
|
9183
|
+
const planningDirPath = join21(directory, PLANNING_DIR2);
|
|
9184
|
+
const codebaseDirectory = codebaseDir(directory);
|
|
9185
|
+
const configPath = join21(planningDirPath, CONFIG_FILE);
|
|
9186
|
+
const statePath3 = join21(planningDirPath, STATE_FILE2);
|
|
9187
|
+
const workspaceRoot = findWorkspaceRoot(directory);
|
|
9188
|
+
if (workspaceRoot && directory !== workspaceRoot) {
|
|
9189
|
+
const workspaceConfig = getWorkspaceConfig(directory);
|
|
9190
|
+
if (workspaceConfig && workspaceConfig.workspace_mode === "shared" && !existsSync21(planningDirPath)) {
|
|
9191
|
+
const msg = `No .planning/ in this sub-repo. Switch to workspace root: cd ${workspaceRoot}`;
|
|
9192
|
+
return deny2(msg, `[flowdeck] BLOCK: ${msg}`, ["workspace-shared-mode"]);
|
|
9193
|
+
}
|
|
9194
|
+
}
|
|
9195
|
+
if (tool13 === "write" || tool13 === "edit") {
|
|
9196
|
+
if (!existsSync21(planningDirPath)) {
|
|
9197
|
+
return allow2("FlowDeck not initialized in this directory — skipping guard-rails");
|
|
9198
|
+
}
|
|
9199
|
+
if (!existsSync21(codebaseDirectory)) {
|
|
9200
|
+
const msg = ".codebase/ not found. Run /fd-map-codebase to map the codebase.";
|
|
9201
|
+
return allow2(msg, ["codebase-missing"]);
|
|
9202
|
+
}
|
|
9203
|
+
const execMode = resolveExecutionMode(configPath, null);
|
|
9204
|
+
if (execMode === "review-only") {
|
|
9205
|
+
const msg = "review-only mode: propose diff but do not apply. Set execution_mode in .planning/config.json to change.";
|
|
9206
|
+
return deny2(msg, `[flowdeck] BLOCK (${msg})`, ["review-only-mode"]);
|
|
9207
|
+
}
|
|
9208
|
+
if (execMode === "guarded") {
|
|
9209
|
+
return allow2("guarded mode: edit will proceed but flag for human review", ["guarded-mode"]);
|
|
9210
|
+
}
|
|
9211
|
+
const designGateMessage = getDesignGateMessage(directory);
|
|
9212
|
+
if (designGateMessage) {
|
|
9213
|
+
if (designGateMessage.startsWith("[flowdeck] WARNING:")) {
|
|
9214
|
+
return allow2(designGateMessage, ["design-gate-advisory"]);
|
|
9215
|
+
}
|
|
9216
|
+
return deny2(designGateMessage, designGateMessage, ["design-gate"]);
|
|
9217
|
+
}
|
|
9218
|
+
const severity = effectiveSeverity(configPath, statePath3);
|
|
9219
|
+
if (severity === null) {
|
|
9220
|
+
return allow2("guard_enforcement is off");
|
|
9221
|
+
}
|
|
9222
|
+
if (severity === "warn") {
|
|
9223
|
+
const warning = getWarningMessage(planningDirPath);
|
|
9224
|
+
return allow2(`[flowdeck] WARNING: ${warning}`, ["plan-not-confirmed"]);
|
|
9225
|
+
}
|
|
9226
|
+
const blockMessage = getBlockMessage(planningDirPath);
|
|
9227
|
+
return deny2(blockMessage, `[flowdeck] BLOCK: ${blockMessage}`, ["plan-not-confirmed"]);
|
|
8729
9228
|
}
|
|
9229
|
+
if (tool13 === "bash") {
|
|
9230
|
+
const cmd = String(args?.command || "");
|
|
9231
|
+
for (const pattern of BUILD_DEPLOY_PATTERNS) {
|
|
9232
|
+
if (cmd.includes(pattern)) {
|
|
9233
|
+
if (!getPlanConfirmed(statePath3)) {
|
|
9234
|
+
const msg = "Build/deploy command detected but plan is not confirmed. Run /fd-plan first.";
|
|
9235
|
+
return allow2(`[flowdeck] WARNING: ${msg}`, ["build-deploy-without-plan"]);
|
|
9236
|
+
}
|
|
9237
|
+
break;
|
|
9238
|
+
}
|
|
9239
|
+
}
|
|
9240
|
+
}
|
|
9241
|
+
return allow2("Guard rails passed");
|
|
8730
9242
|
}
|
|
8731
|
-
function
|
|
8732
|
-
const
|
|
8733
|
-
|
|
8734
|
-
|
|
8735
|
-
|
|
8736
|
-
|
|
8737
|
-
|
|
8738
|
-
|
|
8739
|
-
|
|
8740
|
-
|
|
8741
|
-
|
|
8742
|
-
|
|
8743
|
-
|
|
8744
|
-
}
|
|
8745
|
-
|
|
8746
|
-
// src/services/deadlock-detector.ts
|
|
8747
|
-
function resolveConfig(directory) {
|
|
8748
|
-
try {
|
|
8749
|
-
const config = loadFlowDeckConfig(directory);
|
|
8750
|
-
const dc = config?.governance?.deadlockDetection;
|
|
8751
|
-
return {
|
|
8752
|
-
enabled: dc?.enabled ?? true,
|
|
8753
|
-
bounceThreshold: dc?.bounceThreshold ?? 3,
|
|
8754
|
-
retryLoopThreshold: dc?.retryLoopThreshold ?? 3,
|
|
8755
|
-
stageStallMinutes: dc?.stageStallMinutes ?? 30,
|
|
8756
|
-
autoStop: dc?.autoStop ?? false
|
|
8757
|
-
};
|
|
8758
|
-
} catch {
|
|
8759
|
-
return { enabled: true, bounceThreshold: 3, retryLoopThreshold: 3, stageStallMinutes: 30, autoStop: false };
|
|
9243
|
+
function getDesignGateMessage(dir) {
|
|
9244
|
+
const designConfig = resolveDesignFirstConfig(loadFlowDeckConfig(dir));
|
|
9245
|
+
if (!designConfig.enabled || !designConfig.requireApprovalBeforeImplementation)
|
|
9246
|
+
return null;
|
|
9247
|
+
const state = readPlanningState(dir);
|
|
9248
|
+
if (state.design_override && state.design_override_reason && state.design_override_reason.trim().length > 0)
|
|
9249
|
+
return null;
|
|
9250
|
+
const designApproved = state.design_stage === "handoff_complete" && state.design_approved;
|
|
9251
|
+
if (state.requires_design_first || state.task_type && isUiHeavyTask(state.task_type) || planSuggestsUiHeavy(dir, state.phase || 1)) {
|
|
9252
|
+
if (designApproved)
|
|
9253
|
+
return null;
|
|
9254
|
+
if (designConfig.enforcement === "advisory") {
|
|
9255
|
+
return "[flowdeck] WARNING: UI-heavy task detected without approved design handoff. Run /fd-design --mode=draft first.";
|
|
9256
|
+
}
|
|
9257
|
+
return "[flowdeck] BLOCK: UI-heavy task requires approved design handoff. Run /fd-design --mode=draft or set explicit design override in STATE.md.";
|
|
8760
9258
|
}
|
|
9259
|
+
return null;
|
|
8761
9260
|
}
|
|
8762
|
-
function
|
|
8763
|
-
|
|
9261
|
+
function planSuggestsUiHeavy(dir, phase) {
|
|
9262
|
+
const planPath = phasePlanPath(dir, phase);
|
|
9263
|
+
if (!existsSync21(planPath))
|
|
9264
|
+
return false;
|
|
9265
|
+
const planContent = readFileSync21(planPath, "utf-8");
|
|
9266
|
+
return isUiHeavyTask(planContent);
|
|
8764
9267
|
}
|
|
8765
|
-
function
|
|
8766
|
-
|
|
8767
|
-
|
|
8768
|
-
|
|
8769
|
-
|
|
8770
|
-
|
|
9268
|
+
function effectiveSeverity(configPath, statePath3) {
|
|
9269
|
+
if (existsSync21(configPath)) {
|
|
9270
|
+
try {
|
|
9271
|
+
const configContent = readFileSync21(configPath, "utf-8");
|
|
9272
|
+
const config = JSON.parse(configContent);
|
|
9273
|
+
if (config.guard_enforcement === "warn")
|
|
9274
|
+
return "warn";
|
|
9275
|
+
if (config.guard_enforcement === "block")
|
|
9276
|
+
return "block";
|
|
9277
|
+
if (config.guard_enforcement === "off")
|
|
9278
|
+
return null;
|
|
9279
|
+
} catch {}
|
|
9280
|
+
}
|
|
9281
|
+
return getPlanConfirmed(statePath3) ? "block" : "warn";
|
|
8771
9282
|
}
|
|
8772
|
-
function
|
|
8773
|
-
|
|
8774
|
-
|
|
8775
|
-
return [];
|
|
9283
|
+
function getPlanConfirmed(statePath3) {
|
|
9284
|
+
if (!existsSync21(statePath3))
|
|
9285
|
+
return false;
|
|
8776
9286
|
try {
|
|
8777
|
-
const
|
|
8778
|
-
|
|
8779
|
-
return
|
|
9287
|
+
const content = readFileSync21(statePath3, "utf-8");
|
|
9288
|
+
const match = content.match(/plan_confirmed:\s*(true|false)/i);
|
|
9289
|
+
return match ? match[1].toLowerCase() === "true" : false;
|
|
8780
9290
|
} catch {
|
|
8781
|
-
return
|
|
9291
|
+
return false;
|
|
8782
9292
|
}
|
|
8783
9293
|
}
|
|
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
|
-
}
|
|
9294
|
+
function getWarningMessage(planningDir2) {
|
|
9295
|
+
if (!existsSync21(join21(planningDir2, STATE_FILE2))) {
|
|
9296
|
+
return "No STATE.md found. Run /fd-map-codebase then /fd-new-feature to start a feature.";
|
|
8805
9297
|
}
|
|
8806
|
-
return
|
|
9298
|
+
return "Plan not confirmed. Run /fd-plan and confirm to enable execution.";
|
|
8807
9299
|
}
|
|
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;
|
|
8830
|
-
}
|
|
8831
|
-
const visited = new Set;
|
|
8832
|
-
for (const node of Object.keys(graph)) {
|
|
8833
|
-
if (!visited.has(node)) {
|
|
8834
|
-
const cycle = findCycle(node, visited, [node]);
|
|
8835
|
-
if (cycle) {
|
|
8836
|
-
return {
|
|
8837
|
-
signal_id: randomUUID3(),
|
|
8838
|
-
trace_id,
|
|
8839
|
-
detected_at: new Date().toISOString(),
|
|
8840
|
-
type: "circular_delegation",
|
|
8841
|
-
evidence: [`Delegation cycle: ${cycle.join(" → ")}`],
|
|
8842
|
-
agents_involved: cycle,
|
|
8843
|
-
recommended_action: "stop",
|
|
8844
|
-
auto_stop: true
|
|
8845
|
-
};
|
|
8846
|
-
}
|
|
8847
|
-
}
|
|
9300
|
+
function getBlockMessage(planningDir2) {
|
|
9301
|
+
if (!existsSync21(join21(planningDir2, STATE_FILE2))) {
|
|
9302
|
+
return "No STATE.md found. Run /fd-map-codebase then /fd-new-feature to start a feature.";
|
|
8848
9303
|
}
|
|
8849
|
-
return
|
|
9304
|
+
return "Plan not confirmed. Run /fd-plan and confirm to enable execution.";
|
|
8850
9305
|
}
|
|
8851
|
-
|
|
8852
|
-
|
|
8853
|
-
|
|
8854
|
-
|
|
8855
|
-
|
|
8856
|
-
|
|
8857
|
-
|
|
8858
|
-
|
|
8859
|
-
|
|
8860
|
-
|
|
8861
|
-
|
|
8862
|
-
|
|
8863
|
-
|
|
8864
|
-
|
|
8865
|
-
|
|
8866
|
-
|
|
8867
|
-
|
|
8868
|
-
|
|
8869
|
-
|
|
8870
|
-
|
|
8871
|
-
|
|
8872
|
-
|
|
8873
|
-
|
|
8874
|
-
|
|
9306
|
+
|
|
9307
|
+
// src/services/approval-manager.ts
|
|
9308
|
+
import { existsSync as existsSync22, readFileSync as readFileSync22, writeFileSync as writeFileSync13, mkdirSync as mkdirSync12 } from "fs";
|
|
9309
|
+
import { join as join22 } from "path";
|
|
9310
|
+
import { createHash as createHash4 } from "crypto";
|
|
9311
|
+
import { randomUUID } from "crypto";
|
|
9312
|
+
var APPROVAL_TTL_MS = 30 * 60 * 1000;
|
|
9313
|
+
var SENSITIVE_PATTERNS = [
|
|
9314
|
+
/auth/i,
|
|
9315
|
+
/login/i,
|
|
9316
|
+
/password/i,
|
|
9317
|
+
/secret/i,
|
|
9318
|
+
/token/i,
|
|
9319
|
+
/jwt/i,
|
|
9320
|
+
/session/i,
|
|
9321
|
+
/oauth/i,
|
|
9322
|
+
/payment/i,
|
|
9323
|
+
/billing/i,
|
|
9324
|
+
/stripe/i,
|
|
9325
|
+
/credit/i,
|
|
9326
|
+
/migration/i,
|
|
9327
|
+
/migrate/i,
|
|
9328
|
+
/schema/i,
|
|
9329
|
+
/alembic/i,
|
|
9330
|
+
/infra/i,
|
|
9331
|
+
/terraform/i,
|
|
9332
|
+
/ansible/i,
|
|
9333
|
+
/k8s/i,
|
|
9334
|
+
/kubernetes/i,
|
|
9335
|
+
/docker/i,
|
|
9336
|
+
/\.env/i,
|
|
9337
|
+
/secrets\./i,
|
|
9338
|
+
/config\/prod/i,
|
|
9339
|
+
/production/i,
|
|
9340
|
+
/admin/i,
|
|
9341
|
+
/privilege/i,
|
|
9342
|
+
/sudo/i
|
|
9343
|
+
];
|
|
9344
|
+
function isSensitivePath(filePath) {
|
|
9345
|
+
return SENSITIVE_PATTERNS.some((p) => p.test(filePath));
|
|
9346
|
+
}
|
|
9347
|
+
function approvalsPath(dir) {
|
|
9348
|
+
return join22(codebaseDir(dir), "APPROVALS.json");
|
|
9349
|
+
}
|
|
9350
|
+
function loadStore(dir) {
|
|
9351
|
+
const p = approvalsPath(dir);
|
|
9352
|
+
if (!existsSync22(p))
|
|
9353
|
+
return { requests: [] };
|
|
9354
|
+
try {
|
|
9355
|
+
return JSON.parse(readFileSync22(p, "utf-8"));
|
|
9356
|
+
} catch {
|
|
9357
|
+
return { requests: [] };
|
|
8875
9358
|
}
|
|
8876
|
-
return null;
|
|
8877
9359
|
}
|
|
8878
|
-
function
|
|
8879
|
-
const
|
|
9360
|
+
function saveStore(dir, store) {
|
|
9361
|
+
const cd = codebaseDir(dir);
|
|
9362
|
+
if (!existsSync22(cd))
|
|
9363
|
+
mkdirSync12(cd, { recursive: true });
|
|
9364
|
+
writeFileSync13(approvalsPath(dir), JSON.stringify(store, null, 2), "utf-8");
|
|
9365
|
+
}
|
|
9366
|
+
function requestApproval(dir, run_id, trigger, reason, options = {}) {
|
|
9367
|
+
const store = loadStore(dir);
|
|
9368
|
+
const req = {
|
|
9369
|
+
id: randomUUID(),
|
|
9370
|
+
run_id,
|
|
9371
|
+
session_id: options.session_id ?? "session-0",
|
|
9372
|
+
requested_at: new Date().toISOString(),
|
|
9373
|
+
status: "pending",
|
|
9374
|
+
trigger,
|
|
9375
|
+
reason,
|
|
9376
|
+
risk_score: options.risk_score ?? 0,
|
|
9377
|
+
...options.agent ? { agent: options.agent } : {},
|
|
9378
|
+
...options.file_path ? { file_path: options.file_path } : {},
|
|
9379
|
+
...options.content_hash ? { content_hash: options.content_hash } : {},
|
|
9380
|
+
...options.change_description ? { change_description: options.change_description } : {}
|
|
9381
|
+
};
|
|
9382
|
+
store.requests.push(req);
|
|
9383
|
+
saveStore(dir, store);
|
|
9384
|
+
return req;
|
|
9385
|
+
}
|
|
9386
|
+
function checkApproval(dir, criteria) {
|
|
9387
|
+
const store = loadStore(dir);
|
|
8880
9388
|
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;
|
|
9389
|
+
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
9390
|
}
|
|
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;
|
|
9391
|
+
function computeContentHash(args) {
|
|
9392
|
+
const keys = Object.keys(args).sort();
|
|
9393
|
+
const payload = JSON.stringify(args, keys);
|
|
9394
|
+
return createHash4("sha256").update(payload).digest("hex").slice(0, 16);
|
|
8917
9395
|
}
|
|
8918
|
-
function
|
|
8919
|
-
|
|
9396
|
+
function requestApprovalForTool(dir, run_id, session_id, agent, tool13, args) {
|
|
9397
|
+
const filePath = extractTargetPath2(args);
|
|
9398
|
+
const isSensitive = filePath ? isSensitivePath(filePath) : false;
|
|
9399
|
+
return requestApproval(dir, run_id, tool13, `Approval required for tool "${tool13}"`, {
|
|
9400
|
+
file_path: filePath,
|
|
9401
|
+
risk_score: isSensitive ? 30 : 50,
|
|
9402
|
+
session_id,
|
|
9403
|
+
agent,
|
|
9404
|
+
content_hash: computeContentHash(args),
|
|
9405
|
+
change_description: `Tool "${tool13}" requested on ${filePath || "unknown target"}`
|
|
9406
|
+
});
|
|
9407
|
+
}
|
|
9408
|
+
function extractTargetPath2(args) {
|
|
9409
|
+
return String(args.path ?? args.file_path ?? args.filename ?? args.filePath ?? "");
|
|
8920
9410
|
}
|
|
8921
9411
|
|
|
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);
|
|
9412
|
+
// src/hooks/approval-hook.ts
|
|
9413
|
+
var WRITE_TOOLS = new Set([
|
|
9414
|
+
"write_file",
|
|
9415
|
+
"edit_file",
|
|
9416
|
+
"create_file",
|
|
9417
|
+
"apply_patch",
|
|
9418
|
+
"str_replace_editor",
|
|
9419
|
+
"write",
|
|
9420
|
+
"edit"
|
|
9421
|
+
]);
|
|
9422
|
+
function allow3(reason) {
|
|
9423
|
+
return { verdict: "allow", reason, riskFlags: [], source: "approval-manager" };
|
|
8937
9424
|
}
|
|
8938
|
-
function
|
|
8939
|
-
|
|
8940
|
-
|
|
8941
|
-
|
|
8942
|
-
|
|
9425
|
+
function ask(reason, riskFlags = []) {
|
|
9426
|
+
return {
|
|
9427
|
+
verdict: "ask",
|
|
9428
|
+
reason,
|
|
9429
|
+
riskFlags,
|
|
9430
|
+
source: "approval-manager"
|
|
9431
|
+
};
|
|
8943
9432
|
}
|
|
8944
|
-
function
|
|
8945
|
-
|
|
8946
|
-
|
|
8947
|
-
|
|
8948
|
-
|
|
8949
|
-
|
|
8950
|
-
|
|
8951
|
-
|
|
8952
|
-
|
|
8953
|
-
|
|
8954
|
-
|
|
8955
|
-
|
|
8956
|
-
|
|
8957
|
-
|
|
8958
|
-
|
|
8959
|
-
|
|
8960
|
-
|
|
8961
|
-
|
|
8962
|
-
|
|
8963
|
-
|
|
8964
|
-
}
|
|
9433
|
+
function evaluate3(input) {
|
|
9434
|
+
const { directory, tool: tool13, args } = input;
|
|
9435
|
+
if (!WRITE_TOOLS.has(tool13)) {
|
|
9436
|
+
return allow3("Tool does not require approval");
|
|
9437
|
+
}
|
|
9438
|
+
const filePath = String(args?.path ?? args?.file_path ?? args?.filename ?? args?.filePath ?? "");
|
|
9439
|
+
if (!filePath) {
|
|
9440
|
+
return allow3("No target path — approval not required");
|
|
9441
|
+
}
|
|
9442
|
+
if (!isSensitivePath(filePath)) {
|
|
9443
|
+
return allow3("Path is not sensitive");
|
|
9444
|
+
}
|
|
9445
|
+
const approval = checkApproval(directory, {
|
|
9446
|
+
run_id: input.runId,
|
|
9447
|
+
session_id: input.sessionID,
|
|
9448
|
+
agent: input.agent,
|
|
9449
|
+
file_path: filePath,
|
|
9450
|
+
content_hash: computeContentHash(args)
|
|
9451
|
+
});
|
|
9452
|
+
if (approval) {
|
|
9453
|
+
return allow3(`Approved by ${approval.id}`);
|
|
8965
9454
|
}
|
|
9455
|
+
return ask(`"${filePath}" is a sensitive file (auth/payment/secrets/infra). Manual approval needed before editing.`, ["sensitive-path"]);
|
|
8966
9456
|
}
|
|
8967
|
-
|
|
8968
|
-
|
|
8969
|
-
|
|
8970
|
-
|
|
8971
|
-
|
|
8972
|
-
|
|
9457
|
+
|
|
9458
|
+
// src/services/deadlock-detector.ts
|
|
9459
|
+
import { existsSync as existsSync24, readFileSync as readFileSync24, appendFileSync as appendFileSync5, mkdirSync as mkdirSync14 } from "fs";
|
|
9460
|
+
import { join as join24 } from "path";
|
|
9461
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
9462
|
+
|
|
9463
|
+
// src/services/agent-trace-graph.ts
|
|
9464
|
+
import { existsSync as existsSync23, readFileSync as readFileSync23, appendFileSync as appendFileSync4, writeFileSync as writeFileSync14, mkdirSync as mkdirSync13 } from "fs";
|
|
9465
|
+
import { join as join23 } from "path";
|
|
9466
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
9467
|
+
function agentSpansPath(dir) {
|
|
9468
|
+
return join23(codebaseDir(dir), "AGENT_SPANS.jsonl");
|
|
8973
9469
|
}
|
|
8974
|
-
function
|
|
8975
|
-
const
|
|
8976
|
-
if (!
|
|
9470
|
+
function loadAllSpans(dir) {
|
|
9471
|
+
const p = agentSpansPath(dir);
|
|
9472
|
+
if (!existsSync23(p))
|
|
8977
9473
|
return [];
|
|
8978
9474
|
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;
|
|
9475
|
+
return readFileSync23(p, "utf-8").trim().split(`
|
|
9476
|
+
`).filter(Boolean).map((l) => JSON.parse(l));
|
|
8997
9477
|
} catch {
|
|
8998
9478
|
return [];
|
|
8999
9479
|
}
|
|
9000
9480
|
}
|
|
9001
|
-
function
|
|
9002
|
-
|
|
9003
|
-
|
|
9004
|
-
|
|
9481
|
+
function saveAllSpans(dir, spans) {
|
|
9482
|
+
const p = agentSpansPath(dir);
|
|
9483
|
+
const cd = codebaseDir(dir);
|
|
9484
|
+
if (!existsSync23(cd))
|
|
9485
|
+
mkdirSync13(cd, { recursive: true });
|
|
9486
|
+
writeFileSync14(p, spans.map((s) => JSON.stringify(s)).join(`
|
|
9487
|
+
`) + `
|
|
9488
|
+
`, "utf-8");
|
|
9489
|
+
}
|
|
9490
|
+
function openSpan(dir, opts) {
|
|
9491
|
+
const cd = codebaseDir(dir);
|
|
9492
|
+
if (!existsSync23(cd))
|
|
9493
|
+
mkdirSync13(cd, { recursive: true });
|
|
9494
|
+
const span = {
|
|
9495
|
+
span_id: randomUUID2(),
|
|
9496
|
+
trace_id: opts.trace_id,
|
|
9497
|
+
parent_span_id: opts.parent_span_id,
|
|
9498
|
+
invoker: opts.invoker,
|
|
9499
|
+
agent: opts.agent,
|
|
9500
|
+
task_description: opts.task_description,
|
|
9501
|
+
stage: opts.stage,
|
|
9502
|
+
started_at: new Date().toISOString(),
|
|
9503
|
+
status: "running",
|
|
9504
|
+
output_valid: false,
|
|
9505
|
+
contract_violations: [],
|
|
9506
|
+
tools_used: [],
|
|
9507
|
+
retry_count: 0,
|
|
9508
|
+
depth: opts.depth ?? 0,
|
|
9509
|
+
model: opts.model
|
|
9510
|
+
};
|
|
9511
|
+
appendFileSync4(agentSpansPath(dir), JSON.stringify(span) + `
|
|
9512
|
+
`, "utf-8");
|
|
9513
|
+
return span;
|
|
9514
|
+
}
|
|
9515
|
+
function closeSpan(dir, span_id, status, opts = {}) {
|
|
9516
|
+
const spans = loadAllSpans(dir);
|
|
9517
|
+
const idx = spans.findLastIndex((s) => s.span_id === span_id);
|
|
9518
|
+
if (idx === -1)
|
|
9519
|
+
return;
|
|
9520
|
+
const startedMs = new Date(spans[idx].started_at).getTime();
|
|
9521
|
+
spans[idx] = {
|
|
9522
|
+
...spans[idx],
|
|
9523
|
+
ended_at: new Date().toISOString(),
|
|
9524
|
+
status,
|
|
9525
|
+
latency_ms: Date.now() - startedMs,
|
|
9526
|
+
output_valid: opts.output_valid ?? false,
|
|
9527
|
+
contract_violations: opts.contract_violations ?? spans[idx].contract_violations,
|
|
9528
|
+
tools_used: opts.tools_used ?? spans[idx].tools_used,
|
|
9529
|
+
handoff_payload: opts.handoff_payload,
|
|
9530
|
+
cost_estimate: opts.cost_estimate,
|
|
9531
|
+
retry_count: opts.retry_count ?? spans[idx].retry_count
|
|
9005
9532
|
};
|
|
9533
|
+
saveAllSpans(dir, spans);
|
|
9006
9534
|
}
|
|
9007
|
-
|
|
9008
|
-
|
|
9009
|
-
|
|
9010
|
-
|
|
9535
|
+
function recordToolUsed(dir, span_id, toolName) {
|
|
9536
|
+
const spans = loadAllSpans(dir);
|
|
9537
|
+
const idx = spans.findLastIndex((s) => s.span_id === span_id);
|
|
9538
|
+
if (idx === -1)
|
|
9539
|
+
return;
|
|
9540
|
+
if (!spans[idx].tools_used.includes(toolName)) {
|
|
9541
|
+
spans[idx].tools_used = [...spans[idx].tools_used, toolName];
|
|
9542
|
+
saveAllSpans(dir, spans);
|
|
9543
|
+
}
|
|
9011
9544
|
}
|
|
9012
|
-
function
|
|
9013
|
-
|
|
9014
|
-
|
|
9015
|
-
|
|
9016
|
-
|
|
9017
|
-
|
|
9018
|
-
|
|
9019
|
-
};
|
|
9545
|
+
function addSpanViolation(dir, span_id, violation) {
|
|
9546
|
+
const spans = loadAllSpans(dir);
|
|
9547
|
+
const idx = spans.findLastIndex((s) => s.span_id === span_id);
|
|
9548
|
+
if (idx === -1)
|
|
9549
|
+
return;
|
|
9550
|
+
spans[idx].contract_violations = [...spans[idx].contract_violations, violation];
|
|
9551
|
+
saveAllSpans(dir, spans);
|
|
9020
9552
|
}
|
|
9021
|
-
function
|
|
9022
|
-
|
|
9023
|
-
verdict: "ask",
|
|
9024
|
-
reason,
|
|
9025
|
-
riskFlags,
|
|
9026
|
-
source,
|
|
9027
|
-
approvalRequestId
|
|
9028
|
-
};
|
|
9553
|
+
function recordContractViolation(dir, span_id, violation) {
|
|
9554
|
+
addSpanViolation(dir, span_id, violation);
|
|
9029
9555
|
}
|
|
9030
|
-
function
|
|
9031
|
-
|
|
9032
|
-
|
|
9033
|
-
|
|
9034
|
-
|
|
9035
|
-
|
|
9036
|
-
|
|
9037
|
-
|
|
9038
|
-
|
|
9039
|
-
return
|
|
9556
|
+
function getTraceSpans(dir, trace_id) {
|
|
9557
|
+
return loadAllSpans(dir).filter((s) => s.trace_id === trace_id);
|
|
9558
|
+
}
|
|
9559
|
+
|
|
9560
|
+
// src/services/deadlock-detector.ts
|
|
9561
|
+
function resolveConfig(directory) {
|
|
9562
|
+
try {
|
|
9563
|
+
const config = loadFlowDeckConfig(directory);
|
|
9564
|
+
const dc = config?.governance?.deadlockDetection;
|
|
9565
|
+
return {
|
|
9566
|
+
enabled: dc?.enabled ?? true,
|
|
9567
|
+
bounceThreshold: dc?.bounceThreshold ?? 3,
|
|
9568
|
+
retryLoopThreshold: dc?.retryLoopThreshold ?? 3,
|
|
9569
|
+
stageStallMinutes: dc?.stageStallMinutes ?? 30,
|
|
9570
|
+
autoStop: dc?.autoStop ?? false
|
|
9571
|
+
};
|
|
9572
|
+
} catch {
|
|
9573
|
+
return { enabled: true, bounceThreshold: 3, retryLoopThreshold: 3, stageStallMinutes: 30, autoStop: false };
|
|
9040
9574
|
}
|
|
9041
|
-
return allow4("Agent contract passed", "agent-contract");
|
|
9042
9575
|
}
|
|
9043
|
-
function
|
|
9044
|
-
return
|
|
9045
|
-
id: randomUUID4(),
|
|
9046
|
-
timestamp: new Date().toISOString(),
|
|
9047
|
-
run_id: input.runId,
|
|
9048
|
-
session_id: input.sessionID,
|
|
9049
|
-
agent: input.agent,
|
|
9050
|
-
tool: input.tool,
|
|
9051
|
-
command: input.command,
|
|
9052
|
-
verdict: decision.verdict,
|
|
9053
|
-
reason: decision.reason,
|
|
9054
|
-
risk_flags: decision.riskFlags,
|
|
9055
|
-
source: decision.source,
|
|
9056
|
-
approval_request_id: decision.approvalRequestId
|
|
9057
|
-
};
|
|
9576
|
+
function deadlockSignalsPath(dir) {
|
|
9577
|
+
return join24(codebaseDir(dir), "DEADLOCK_SIGNALS.jsonl");
|
|
9058
9578
|
}
|
|
9059
|
-
function
|
|
9060
|
-
const
|
|
9061
|
-
|
|
9062
|
-
|
|
9063
|
-
|
|
9064
|
-
|
|
9065
|
-
|
|
9066
|
-
|
|
9067
|
-
|
|
9068
|
-
|
|
9069
|
-
|
|
9070
|
-
|
|
9071
|
-
|
|
9072
|
-
|
|
9073
|
-
|
|
9074
|
-
|
|
9075
|
-
|
|
9076
|
-
}
|
|
9077
|
-
}
|
|
9078
|
-
function checkRuntimeLimits(input) {
|
|
9079
|
-
try {
|
|
9080
|
-
const loop = loopDetector.checkBefore(input.tool, input.args, input.sessionID);
|
|
9081
|
-
if (loop.action === "block") {
|
|
9082
|
-
return deny3(loop.reason, "loop-detector", loop.escalationMessage, ["loop-detected"]);
|
|
9083
|
-
}
|
|
9084
|
-
if (loop.action === "warn") {
|
|
9085
|
-
return allow4(loop.message, "loop-detector", ["loop-warning"]);
|
|
9086
|
-
}
|
|
9087
|
-
if (isTraceStuck(input.directory, input.runId)) {
|
|
9088
|
-
return deny3("Run is stuck (deadlock signal with auto_stop)", "deadlock-detector", "Deadlock detection flagged this run for auto-stop. Escalate to the human.", ["deadlock"]);
|
|
9089
|
-
}
|
|
9090
|
-
return allow4("Runtime limits passed", "runtime-limits");
|
|
9091
|
-
} catch (error) {
|
|
9092
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
9093
|
-
if (appLog)
|
|
9094
|
-
appLog(`[harness-policy] runtime limits error: ${message}`);
|
|
9095
|
-
return deny3(`Runtime limits check failed: ${message}`, "runtime-limits", `Runtime limits check failed: ${message}`, ["runtime-check-error"]);
|
|
9096
|
-
}
|
|
9097
|
-
}
|
|
9098
|
-
function checkAgentContract(input) {
|
|
9099
|
-
try {
|
|
9100
|
-
const result = validateToolAccess(input.directory, input.agent, input.tool, { run_id: input.runId, session_id: input.sessionID });
|
|
9101
|
-
return validationToDecision(result);
|
|
9102
|
-
} catch (error) {
|
|
9103
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
9104
|
-
if (appLog)
|
|
9105
|
-
appLog(`[harness-policy] agent contract error: ${message}`);
|
|
9106
|
-
return deny3(`Agent contract check failed: ${message}`, "agent-contract", `Agent contract check failed: ${message}`, ["agent-contract-error"]);
|
|
9107
|
-
}
|
|
9108
|
-
}
|
|
9109
|
-
function evaluatePolicyEngine(input) {
|
|
9110
|
-
try {
|
|
9111
|
-
const store = readStore2(input.directory);
|
|
9112
|
-
const filePath = extractTargetPath(input.args);
|
|
9113
|
-
const active = store.policies.filter((p) => p.active);
|
|
9114
|
-
for (const policy of active) {
|
|
9115
|
-
const trigger = policy.trigger.toLowerCase();
|
|
9116
|
-
if (!trigger.trim())
|
|
9117
|
-
continue;
|
|
9118
|
-
if (input.tool.toLowerCase().includes(trigger) || filePath.toLowerCase().includes(trigger)) {
|
|
9119
|
-
return deny3(`Active policy violation: ${policy.name}`, "policy-engine", policy.rule, ["policy-violation", policy.id]);
|
|
9120
|
-
}
|
|
9121
|
-
}
|
|
9122
|
-
return allow4("No active policy violations", "policy-engine");
|
|
9123
|
-
} catch (error) {
|
|
9124
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
9125
|
-
if (appLog)
|
|
9126
|
-
appLog(`[harness-policy] policy engine error: ${message}`);
|
|
9127
|
-
return allow4(`Policy engine check failed: ${message}`, "policy-engine", [
|
|
9128
|
-
"policy-engine-error"
|
|
9129
|
-
]);
|
|
9130
|
-
}
|
|
9131
|
-
}
|
|
9132
|
-
function checkGovernance(input) {
|
|
9133
|
-
try {
|
|
9134
|
-
const riskFlags = [];
|
|
9135
|
-
if (config.governance?.toolGuard !== false) {
|
|
9136
|
-
const tg = evaluate(input);
|
|
9137
|
-
if (tg.verdict === "deny")
|
|
9138
|
-
return tg;
|
|
9139
|
-
riskFlags.push(...tg.riskFlags);
|
|
9140
|
-
}
|
|
9141
|
-
if (config.governance?.guardRails !== false) {
|
|
9142
|
-
const gr = evaluate2(input);
|
|
9143
|
-
if (gr.verdict === "deny")
|
|
9144
|
-
return gr;
|
|
9145
|
-
riskFlags.push(...gr.riskFlags);
|
|
9146
|
-
}
|
|
9147
|
-
if (config.governance?.approvals !== false) {
|
|
9148
|
-
const ap = evaluate3(input);
|
|
9149
|
-
if (ap.verdict !== "allow")
|
|
9150
|
-
return ap;
|
|
9151
|
-
}
|
|
9152
|
-
const pe = evaluatePolicyEngine(input);
|
|
9153
|
-
if (pe.verdict === "deny")
|
|
9154
|
-
return pe;
|
|
9155
|
-
riskFlags.push(...pe.riskFlags);
|
|
9156
|
-
return allow4("Governance checks passed", "governance", riskFlags);
|
|
9157
|
-
} catch (error) {
|
|
9158
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
9159
|
-
if (appLog)
|
|
9160
|
-
appLog(`[harness-policy] governance error: ${message}`);
|
|
9161
|
-
return deny3(`Governance check failed: ${message}`, "governance", `Governance check failed: ${message}`, ["governance-error"]);
|
|
9162
|
-
}
|
|
9163
|
-
}
|
|
9164
|
-
function checkSupervisor(input, _targetName) {
|
|
9165
|
-
if (config.governance?.supervisor?.enabled === false) {
|
|
9166
|
-
return allow4("Supervisor disabled", "supervisor");
|
|
9167
|
-
}
|
|
9168
|
-
try {
|
|
9169
|
-
return reviewToolCall(input.directory, input);
|
|
9170
|
-
} catch (error) {
|
|
9171
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
9172
|
-
if (appLog)
|
|
9173
|
-
appLog(`[harness-policy] supervisor error: ${message}`);
|
|
9174
|
-
return deny3(`Supervisor review failed: ${message}`, "supervisor", `Supervisor review failed: ${message}`, ["supervisor-error"]);
|
|
9175
|
-
}
|
|
9176
|
-
}
|
|
9177
|
-
function generateApprovalRequestId(input) {
|
|
9178
|
-
try {
|
|
9179
|
-
const req = requestApprovalForTool(input.directory, input.runId, input.sessionID, input.agent, input.tool, input.args);
|
|
9180
|
-
return req.id;
|
|
9181
|
-
} catch (error) {
|
|
9182
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
9183
|
-
if (appLog)
|
|
9184
|
-
appLog(`[harness-policy] approval request failed: ${message}`);
|
|
9185
|
-
return;
|
|
9186
|
-
}
|
|
9579
|
+
function appendSignal(dir, signal) {
|
|
9580
|
+
const cd = codebaseDir(dir);
|
|
9581
|
+
if (!existsSync24(cd))
|
|
9582
|
+
mkdirSync14(cd, { recursive: true });
|
|
9583
|
+
appendFileSync5(deadlockSignalsPath(dir), JSON.stringify(signal) + `
|
|
9584
|
+
`, "utf-8");
|
|
9585
|
+
}
|
|
9586
|
+
function getSignals(dir, trace_id) {
|
|
9587
|
+
const p = deadlockSignalsPath(dir);
|
|
9588
|
+
if (!existsSync24(p))
|
|
9589
|
+
return [];
|
|
9590
|
+
try {
|
|
9591
|
+
const all = readFileSync24(p, "utf-8").trim().split(`
|
|
9592
|
+
`).filter(Boolean).map((l) => JSON.parse(l));
|
|
9593
|
+
return trace_id ? all.filter((s) => s.trace_id === trace_id) : all;
|
|
9594
|
+
} catch {
|
|
9595
|
+
return [];
|
|
9187
9596
|
}
|
|
9188
|
-
|
|
9189
|
-
|
|
9190
|
-
|
|
9191
|
-
|
|
9192
|
-
|
|
9193
|
-
|
|
9597
|
+
}
|
|
9598
|
+
function detectAgentBounce(dir, trace_id, cfg) {
|
|
9599
|
+
const spans = getTraceSpans(dir, trace_id);
|
|
9600
|
+
const pairCounts = {};
|
|
9601
|
+
for (let i = 1;i < spans.length; i++) {
|
|
9602
|
+
const key = `${spans[i - 1].agent}→${spans[i].agent}`;
|
|
9603
|
+
pairCounts[key] = (pairCounts[key] ?? 0) + 1;
|
|
9604
|
+
}
|
|
9605
|
+
for (const [pair, count] of Object.entries(pairCounts)) {
|
|
9606
|
+
if (count >= cfg.bounceThreshold) {
|
|
9607
|
+
const [a, b] = pair.split("→");
|
|
9194
9608
|
return {
|
|
9195
|
-
|
|
9196
|
-
|
|
9609
|
+
signal_id: randomUUID3(),
|
|
9610
|
+
trace_id,
|
|
9611
|
+
detected_at: new Date().toISOString(),
|
|
9612
|
+
type: "agent_bounce",
|
|
9613
|
+
evidence: [`Agent pair "${pair}" handed off ${count} times (threshold: ${cfg.bounceThreshold})`],
|
|
9614
|
+
agents_involved: [a, b],
|
|
9615
|
+
recommended_action: "escalate_human",
|
|
9616
|
+
auto_stop: cfg.autoStop
|
|
9197
9617
|
};
|
|
9198
9618
|
}
|
|
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
9619
|
}
|
|
9203
|
-
|
|
9204
|
-
|
|
9205
|
-
|
|
9206
|
-
|
|
9207
|
-
|
|
9208
|
-
|
|
9209
|
-
|
|
9210
|
-
|
|
9211
|
-
|
|
9212
|
-
|
|
9213
|
-
|
|
9214
|
-
|
|
9215
|
-
|
|
9216
|
-
|
|
9217
|
-
|
|
9218
|
-
|
|
9219
|
-
|
|
9220
|
-
|
|
9221
|
-
|
|
9222
|
-
|
|
9223
|
-
|
|
9224
|
-
return decision2;
|
|
9620
|
+
return null;
|
|
9621
|
+
}
|
|
9622
|
+
function detectCircularDelegation(dir, trace_id, cfg) {
|
|
9623
|
+
const spans = getTraceSpans(dir, trace_id);
|
|
9624
|
+
const graph = {};
|
|
9625
|
+
for (const span of spans) {
|
|
9626
|
+
if (span.invoker === span.agent)
|
|
9627
|
+
continue;
|
|
9628
|
+
if (!graph[span.invoker])
|
|
9629
|
+
graph[span.invoker] = new Set;
|
|
9630
|
+
graph[span.invoker].add(span.agent);
|
|
9631
|
+
}
|
|
9632
|
+
function findCycle(node, visited2, stack) {
|
|
9633
|
+
visited2.add(node);
|
|
9634
|
+
for (const neighbor of [...graph[node] ?? []]) {
|
|
9635
|
+
if (stack.includes(neighbor))
|
|
9636
|
+
return [...stack, neighbor];
|
|
9637
|
+
if (!visited2.has(neighbor)) {
|
|
9638
|
+
const result = findCycle(neighbor, visited2, [...stack, neighbor]);
|
|
9639
|
+
if (result)
|
|
9640
|
+
return result;
|
|
9225
9641
|
}
|
|
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
9642
|
}
|
|
9243
|
-
|
|
9244
|
-
|
|
9245
|
-
|
|
9246
|
-
|
|
9247
|
-
|
|
9643
|
+
return null;
|
|
9644
|
+
}
|
|
9645
|
+
const visited = new Set;
|
|
9646
|
+
for (const node of Object.keys(graph)) {
|
|
9647
|
+
if (!visited.has(node)) {
|
|
9648
|
+
const cycle = findCycle(node, visited, [node]);
|
|
9649
|
+
if (cycle) {
|
|
9650
|
+
return {
|
|
9651
|
+
signal_id: randomUUID3(),
|
|
9652
|
+
trace_id,
|
|
9653
|
+
detected_at: new Date().toISOString(),
|
|
9654
|
+
type: "circular_delegation",
|
|
9655
|
+
evidence: [`Delegation cycle: ${cycle.join(" → ")}`],
|
|
9656
|
+
agents_involved: cycle,
|
|
9657
|
+
recommended_action: "stop",
|
|
9658
|
+
auto_stop: true
|
|
9659
|
+
};
|
|
9660
|
+
}
|
|
9248
9661
|
}
|
|
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
9662
|
}
|
|
9255
|
-
return
|
|
9256
|
-
evaluate: evaluate4,
|
|
9257
|
-
checkAgentContract,
|
|
9258
|
-
checkRuntimeLimits,
|
|
9259
|
-
checkGovernance,
|
|
9260
|
-
checkSupervisor
|
|
9261
|
-
};
|
|
9663
|
+
return null;
|
|
9262
9664
|
}
|
|
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"]
|
|
9665
|
+
function detectStepRetryLoop(dir, trace_id, cfg) {
|
|
9666
|
+
const spans = getTraceSpans(dir, trace_id);
|
|
9667
|
+
const stageCounts = {};
|
|
9668
|
+
const stageAgents = {};
|
|
9669
|
+
for (const span of spans) {
|
|
9670
|
+
const key = `${span.agent}:${span.stage}`;
|
|
9671
|
+
stageCounts[key] = (stageCounts[key] ?? 0) + 1;
|
|
9672
|
+
if (!stageAgents[key])
|
|
9673
|
+
stageAgents[key] = new Set;
|
|
9674
|
+
stageAgents[key].add(span.agent);
|
|
9320
9675
|
}
|
|
9321
|
-
]
|
|
9322
|
-
|
|
9323
|
-
|
|
9324
|
-
|
|
9325
|
-
|
|
9326
|
-
|
|
9327
|
-
|
|
9328
|
-
|
|
9329
|
-
|
|
9676
|
+
for (const [key, count] of Object.entries(stageCounts)) {
|
|
9677
|
+
if (count >= cfg.retryLoopThreshold) {
|
|
9678
|
+
return {
|
|
9679
|
+
signal_id: randomUUID3(),
|
|
9680
|
+
trace_id,
|
|
9681
|
+
detected_at: new Date().toISOString(),
|
|
9682
|
+
type: "step_retry_loop",
|
|
9683
|
+
evidence: [`Stage "${key}" executed ${count} times (threshold: ${cfg.retryLoopThreshold})`],
|
|
9684
|
+
agents_involved: [...stageAgents[key] ?? new Set],
|
|
9685
|
+
recommended_action: "escalate_human",
|
|
9686
|
+
auto_stop: cfg.autoStop
|
|
9687
|
+
};
|
|
9688
|
+
}
|
|
9330
9689
|
}
|
|
9331
|
-
|
|
9332
|
-
|
|
9333
|
-
|
|
9334
|
-
|
|
9335
|
-
|
|
9336
|
-
|
|
9337
|
-
|
|
9338
|
-
|
|
9339
|
-
|
|
9340
|
-
|
|
9341
|
-
|
|
9690
|
+
return null;
|
|
9691
|
+
}
|
|
9692
|
+
function detectStageStall(dir, trace_id, cfg) {
|
|
9693
|
+
const spans = getTraceSpans(dir, trace_id);
|
|
9694
|
+
const now = Date.now();
|
|
9695
|
+
for (const span of spans) {
|
|
9696
|
+
if (span.status !== "running")
|
|
9697
|
+
continue;
|
|
9698
|
+
const elapsed = (now - new Date(span.started_at).getTime()) / 1000 / 60;
|
|
9699
|
+
if (elapsed >= cfg.stageStallMinutes) {
|
|
9700
|
+
return {
|
|
9701
|
+
signal_id: randomUUID3(),
|
|
9702
|
+
trace_id,
|
|
9703
|
+
detected_at: new Date().toISOString(),
|
|
9704
|
+
type: "stage_stall",
|
|
9705
|
+
evidence: [
|
|
9706
|
+
`Agent "${span.agent}" in stage "${span.stage}" running for ${Math.round(elapsed)}min (threshold: ${cfg.stageStallMinutes}min)`
|
|
9707
|
+
],
|
|
9708
|
+
agents_involved: [span.agent],
|
|
9709
|
+
recommended_action: "escalate_human",
|
|
9710
|
+
auto_stop: cfg.autoStop
|
|
9711
|
+
};
|
|
9712
|
+
}
|
|
9342
9713
|
}
|
|
9343
|
-
return
|
|
9714
|
+
return null;
|
|
9715
|
+
}
|
|
9716
|
+
function detectDeadlocks(dir, trace_id) {
|
|
9717
|
+
const cfg = resolveConfig(dir);
|
|
9718
|
+
if (!cfg.enabled)
|
|
9719
|
+
return [];
|
|
9720
|
+
const existingTypes = new Set(getSignals(dir, trace_id).map((s) => s.type));
|
|
9721
|
+
const candidates = [
|
|
9722
|
+
detectAgentBounce(dir, trace_id, cfg),
|
|
9723
|
+
detectCircularDelegation(dir, trace_id, cfg),
|
|
9724
|
+
detectStepRetryLoop(dir, trace_id, cfg),
|
|
9725
|
+
detectStageStall(dir, trace_id, cfg)
|
|
9726
|
+
];
|
|
9727
|
+
const newSignals = candidates.filter((s) => s !== null && !existingTypes.has(s.type));
|
|
9728
|
+
for (const signal of newSignals)
|
|
9729
|
+
appendSignal(dir, signal);
|
|
9730
|
+
return newSignals;
|
|
9344
9731
|
}
|
|
9345
|
-
function
|
|
9346
|
-
|
|
9347
|
-
|
|
9348
|
-
|
|
9349
|
-
|
|
9350
|
-
|
|
9351
|
-
|
|
9352
|
-
|
|
9732
|
+
function isTraceStuck(dir, trace_id) {
|
|
9733
|
+
return getSignals(dir, trace_id).some((s) => s.auto_stop);
|
|
9734
|
+
}
|
|
9735
|
+
|
|
9736
|
+
// src/services/audit-log.ts
|
|
9737
|
+
import {
|
|
9738
|
+
existsSync as existsSync25,
|
|
9739
|
+
mkdirSync as mkdirSync15,
|
|
9740
|
+
appendFileSync as appendFileSync6,
|
|
9741
|
+
readFileSync as readFileSync25,
|
|
9742
|
+
writeFileSync as writeFileSync15,
|
|
9743
|
+
renameSync,
|
|
9744
|
+
unlinkSync
|
|
9745
|
+
} from "fs";
|
|
9746
|
+
import { join as join25 } from "path";
|
|
9747
|
+
var AUDIT_FILE = "AUDIT.jsonl";
|
|
9748
|
+
var ROTATE_LINE_COUNT = 1000;
|
|
9749
|
+
function auditPath(directory) {
|
|
9750
|
+
return join25(codebaseDir(directory), AUDIT_FILE);
|
|
9751
|
+
}
|
|
9752
|
+
function ensureDirectory(dir) {
|
|
9753
|
+
const base = codebaseDir(dir);
|
|
9754
|
+
if (!existsSync25(base)) {
|
|
9755
|
+
mkdirSync15(base, { recursive: true });
|
|
9353
9756
|
}
|
|
9354
|
-
|
|
9355
|
-
|
|
9356
|
-
|
|
9357
|
-
|
|
9358
|
-
|
|
9359
|
-
|
|
9757
|
+
}
|
|
9758
|
+
function rotateIfNeeded(path, appLog) {
|
|
9759
|
+
try {
|
|
9760
|
+
const content = readFileSync25(path, "utf-8");
|
|
9761
|
+
const lines = content.split(`
|
|
9762
|
+
`).filter((line) => line.trim().length > 0);
|
|
9763
|
+
if (lines.length <= ROTATE_LINE_COUNT)
|
|
9764
|
+
return;
|
|
9765
|
+
const backupPath = `${path}.backup`;
|
|
9766
|
+
renameSync(path, backupPath);
|
|
9767
|
+
const keep = lines.slice(-ROTATE_LINE_COUNT);
|
|
9768
|
+
writeFileSync15(path, keep.join(`
|
|
9769
|
+
`) + `
|
|
9770
|
+
`, "utf-8");
|
|
9771
|
+
try {
|
|
9772
|
+
unlinkSync(backupPath);
|
|
9773
|
+
} catch {}
|
|
9774
|
+
} catch (error) {
|
|
9775
|
+
if (appLog) {
|
|
9776
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
9777
|
+
appLog(`[audit-log] rotation failed: ${message}`);
|
|
9778
|
+
}
|
|
9360
9779
|
}
|
|
9361
|
-
|
|
9362
|
-
|
|
9363
|
-
|
|
9364
|
-
|
|
9365
|
-
|
|
9366
|
-
|
|
9367
|
-
|
|
9368
|
-
|
|
9369
|
-
|
|
9370
|
-
|
|
9371
|
-
|
|
9780
|
+
}
|
|
9781
|
+
function appendAuditEntry(directory, entry, appLog) {
|
|
9782
|
+
ensureDirectory(directory);
|
|
9783
|
+
const path = auditPath(directory);
|
|
9784
|
+
appendFileSync6(path, JSON.stringify(entry) + `
|
|
9785
|
+
`, "utf-8");
|
|
9786
|
+
rotateIfNeeded(path, appLog);
|
|
9787
|
+
}
|
|
9788
|
+
function queryAudit(directory, filter) {
|
|
9789
|
+
const path = auditPath(directory);
|
|
9790
|
+
if (!existsSync25(path))
|
|
9791
|
+
return [];
|
|
9792
|
+
try {
|
|
9793
|
+
const lines = readFileSync25(path, "utf-8").split(`
|
|
9794
|
+
`).filter((line) => line.trim().length > 0);
|
|
9795
|
+
const results = [];
|
|
9796
|
+
for (const line of lines) {
|
|
9797
|
+
try {
|
|
9798
|
+
const entry = JSON.parse(line);
|
|
9799
|
+
if (filter.run_id && entry.run_id !== filter.run_id)
|
|
9800
|
+
continue;
|
|
9801
|
+
if (filter.session_id && entry.session_id !== filter.session_id)
|
|
9802
|
+
continue;
|
|
9803
|
+
if (filter.tool && entry.tool !== filter.tool)
|
|
9804
|
+
continue;
|
|
9805
|
+
if (filter.verdict && entry.verdict !== filter.verdict)
|
|
9806
|
+
continue;
|
|
9807
|
+
results.push(entry);
|
|
9808
|
+
} catch {}
|
|
9809
|
+
}
|
|
9810
|
+
return results;
|
|
9811
|
+
} catch {
|
|
9812
|
+
return [];
|
|
9372
9813
|
}
|
|
9814
|
+
}
|
|
9815
|
+
function createAuditLog(directory, appLog) {
|
|
9373
9816
|
return {
|
|
9374
|
-
|
|
9375
|
-
|
|
9817
|
+
append: (entry) => appendAuditEntry(directory, entry, appLog),
|
|
9818
|
+
query: (filter) => queryAudit(directory, filter)
|
|
9376
9819
|
};
|
|
9377
9820
|
}
|
|
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
9821
|
|
|
9405
|
-
// src/services/
|
|
9406
|
-
function
|
|
9407
|
-
return {
|
|
9822
|
+
// src/services/harness-policy.ts
|
|
9823
|
+
function allow4(reason, source, riskFlags = []) {
|
|
9824
|
+
return { verdict: "allow", reason, riskFlags, source };
|
|
9408
9825
|
}
|
|
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;
|
|
9826
|
+
function deny3(reason, source, escalationMessage, riskFlags = []) {
|
|
9416
9827
|
return {
|
|
9417
|
-
|
|
9418
|
-
|
|
9419
|
-
|
|
9420
|
-
|
|
9421
|
-
|
|
9422
|
-
total
|
|
9828
|
+
verdict: "deny",
|
|
9829
|
+
reason,
|
|
9830
|
+
riskFlags,
|
|
9831
|
+
source,
|
|
9832
|
+
escalationMessage
|
|
9423
9833
|
};
|
|
9424
9834
|
}
|
|
9425
|
-
function
|
|
9426
|
-
|
|
9427
|
-
|
|
9428
|
-
|
|
9429
|
-
|
|
9430
|
-
|
|
9431
|
-
|
|
9432
|
-
|
|
9433
|
-
|
|
9434
|
-
|
|
9435
|
-
|
|
9436
|
-
|
|
9437
|
-
|
|
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}`;
|
|
9835
|
+
function ask2(reason, source, approvalRequestId, riskFlags = []) {
|
|
9836
|
+
return {
|
|
9837
|
+
verdict: "ask",
|
|
9838
|
+
reason,
|
|
9839
|
+
riskFlags,
|
|
9840
|
+
source,
|
|
9841
|
+
approvalRequestId
|
|
9842
|
+
};
|
|
9843
|
+
}
|
|
9844
|
+
function validationToDecision(result) {
|
|
9845
|
+
const riskFlags = result.violations.map((v) => v.rule);
|
|
9846
|
+
if (result.action === "block") {
|
|
9847
|
+
return deny3(result.message ?? "Agent contract violation", "agent-contract", result.message ?? "This tool call violates the agent's capability contract", riskFlags);
|
|
9488
9848
|
}
|
|
9849
|
+
if (result.action === "escalate") {
|
|
9850
|
+
return ask2(result.message ?? "Agent contract requires escalation", "agent-contract", undefined, riskFlags);
|
|
9851
|
+
}
|
|
9852
|
+
if (result.action === "warn") {
|
|
9853
|
+
return allow4(result.message ?? "Agent contract advisory", "agent-contract", riskFlags);
|
|
9854
|
+
}
|
|
9855
|
+
return allow4("Agent contract passed", "agent-contract");
|
|
9856
|
+
}
|
|
9857
|
+
function buildAuditEntry(input, decision) {
|
|
9489
9858
|
return {
|
|
9490
|
-
|
|
9491
|
-
|
|
9492
|
-
|
|
9493
|
-
|
|
9494
|
-
|
|
9859
|
+
id: randomUUID4(),
|
|
9860
|
+
timestamp: new Date().toISOString(),
|
|
9861
|
+
run_id: input.runId,
|
|
9862
|
+
session_id: input.sessionID,
|
|
9863
|
+
agent: input.agent,
|
|
9864
|
+
tool: input.tool,
|
|
9865
|
+
command: input.command,
|
|
9866
|
+
verdict: decision.verdict,
|
|
9867
|
+
reason: decision.reason,
|
|
9868
|
+
risk_flags: decision.riskFlags,
|
|
9869
|
+
source: decision.source,
|
|
9870
|
+
approval_request_id: decision.approvalRequestId
|
|
9495
9871
|
};
|
|
9496
9872
|
}
|
|
9497
|
-
|
|
9498
|
-
|
|
9499
|
-
|
|
9500
|
-
|
|
9501
|
-
|
|
9502
|
-
|
|
9503
|
-
|
|
9504
|
-
|
|
9505
|
-
|
|
9506
|
-
|
|
9507
|
-
|
|
9508
|
-
|
|
9509
|
-
|
|
9510
|
-
|
|
9511
|
-
|
|
9512
|
-
|
|
9513
|
-
|
|
9514
|
-
|
|
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.");
|
|
9599
|
-
}
|
|
9600
|
-
const bugHits = BUG_SIGNALS.filter((s) => lower.includes(s));
|
|
9601
|
-
const uiHits = UI_SIGNALS.filter((s) => lower.includes(s));
|
|
9602
|
-
const docsHits = DOCS_SIGNALS.filter((s) => lower.includes(s));
|
|
9603
|
-
const simpleHits = SIMPLE_SIGNALS.filter((s) => lower.includes(s));
|
|
9604
|
-
const bugScore = Math.min(bugHits.length * 0.35, 1);
|
|
9605
|
-
const uiScore = Math.min(uiHits.length * 0.3, 1);
|
|
9606
|
-
const docsScore = Math.min(docsHits.length * 0.4, 1);
|
|
9607
|
-
const simpleScore = Math.min(simpleHits.length * 0.45, 1);
|
|
9608
|
-
if (bugScore >= 0.35 && bugScore >= uiScore && bugScore >= docsScore) {
|
|
9609
|
-
return {
|
|
9610
|
-
taskType: "bugfix",
|
|
9611
|
-
confidence: Math.min(0.5 + bugScore * 0.5, 0.98),
|
|
9612
|
-
signals: bugHits,
|
|
9613
|
-
requiresDesign: false,
|
|
9614
|
-
requiresTDD: true,
|
|
9615
|
-
stageSequence: buildStageSequence("bugfix"),
|
|
9616
|
-
clarificationNeeded: bugScore < 0.5,
|
|
9617
|
-
clarificationPrompt: bugScore < 0.5 ? "Can you describe the specific bug? What is the expected vs actual behavior?" : undefined
|
|
9618
|
-
};
|
|
9873
|
+
function createHarnessPolicy(directory, appLog) {
|
|
9874
|
+
const config = loadFlowDeckConfig(directory);
|
|
9875
|
+
const auditLog = createAuditLog(directory, appLog);
|
|
9876
|
+
const loopDetector = new LoopDetector({
|
|
9877
|
+
enabled: config.governance?.loopDetection?.enabled ?? true,
|
|
9878
|
+
maxRepeats: config.governance?.loopDetection?.maxRepeats ?? 2,
|
|
9879
|
+
similarityThreshold: config.governance?.loopDetection?.similarityThreshold ?? 0.9,
|
|
9880
|
+
historySize: config.governance?.loopDetection?.historySize ?? 20
|
|
9881
|
+
}, appLog);
|
|
9882
|
+
function appendAudit(input, decision) {
|
|
9883
|
+
try {
|
|
9884
|
+
auditLog.append(buildAuditEntry(input, decision));
|
|
9885
|
+
} catch (error) {
|
|
9886
|
+
if (appLog) {
|
|
9887
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
9888
|
+
appLog(`[harness-policy] audit append failed: ${message}`);
|
|
9889
|
+
}
|
|
9890
|
+
}
|
|
9619
9891
|
}
|
|
9620
|
-
|
|
9621
|
-
|
|
9622
|
-
|
|
9623
|
-
|
|
9624
|
-
|
|
9625
|
-
|
|
9626
|
-
|
|
9627
|
-
|
|
9628
|
-
|
|
9629
|
-
|
|
9892
|
+
function checkRuntimeLimits(input) {
|
|
9893
|
+
try {
|
|
9894
|
+
const loop = loopDetector.checkBefore(input.tool, input.args, input.sessionID);
|
|
9895
|
+
if (loop.action === "block") {
|
|
9896
|
+
return deny3(loop.reason, "loop-detector", loop.escalationMessage, ["loop-detected"]);
|
|
9897
|
+
}
|
|
9898
|
+
if (loop.action === "warn") {
|
|
9899
|
+
return allow4(loop.message, "loop-detector", ["loop-warning"]);
|
|
9900
|
+
}
|
|
9901
|
+
if (isTraceStuck(input.directory, input.runId)) {
|
|
9902
|
+
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"]);
|
|
9903
|
+
}
|
|
9904
|
+
return allow4("Runtime limits passed", "runtime-limits");
|
|
9905
|
+
} catch (error) {
|
|
9906
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
9907
|
+
if (appLog)
|
|
9908
|
+
appLog(`[harness-policy] runtime limits error: ${message}`);
|
|
9909
|
+
return deny3(`Runtime limits check failed: ${message}`, "runtime-limits", `Runtime limits check failed: ${message}`, ["runtime-check-error"]);
|
|
9910
|
+
}
|
|
9630
9911
|
}
|
|
9631
|
-
|
|
9632
|
-
|
|
9633
|
-
|
|
9634
|
-
|
|
9635
|
-
|
|
9636
|
-
|
|
9637
|
-
|
|
9638
|
-
|
|
9639
|
-
|
|
9640
|
-
}
|
|
9912
|
+
function checkAgentContract(input) {
|
|
9913
|
+
try {
|
|
9914
|
+
const result = validateToolAccess(input.directory, input.agent, input.tool, { run_id: input.runId, session_id: input.sessionID });
|
|
9915
|
+
return validationToDecision(result);
|
|
9916
|
+
} catch (error) {
|
|
9917
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
9918
|
+
if (appLog)
|
|
9919
|
+
appLog(`[harness-policy] agent contract error: ${message}`);
|
|
9920
|
+
return deny3(`Agent contract check failed: ${message}`, "agent-contract", `Agent contract check failed: ${message}`, ["agent-contract-error"]);
|
|
9921
|
+
}
|
|
9641
9922
|
}
|
|
9642
|
-
|
|
9643
|
-
|
|
9644
|
-
|
|
9645
|
-
|
|
9646
|
-
|
|
9647
|
-
|
|
9648
|
-
|
|
9649
|
-
|
|
9650
|
-
|
|
9651
|
-
|
|
9923
|
+
function evaluatePolicyEngine(input) {
|
|
9924
|
+
try {
|
|
9925
|
+
const store = readStore2(input.directory);
|
|
9926
|
+
const filePath = extractTargetPath(input.args);
|
|
9927
|
+
const active = store.policies.filter((p) => p.active);
|
|
9928
|
+
for (const policy of active) {
|
|
9929
|
+
const trigger = policy.trigger.toLowerCase();
|
|
9930
|
+
if (!trigger.trim())
|
|
9931
|
+
continue;
|
|
9932
|
+
if (input.tool.toLowerCase().includes(trigger) || filePath.toLowerCase().includes(trigger)) {
|
|
9933
|
+
return deny3(`Active policy violation: ${policy.name}`, "policy-engine", policy.rule, ["policy-violation", policy.id]);
|
|
9934
|
+
}
|
|
9935
|
+
}
|
|
9936
|
+
return allow4("No active policy violations", "policy-engine");
|
|
9937
|
+
} catch (error) {
|
|
9938
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
9939
|
+
if (appLog)
|
|
9940
|
+
appLog(`[harness-policy] policy engine error: ${message}`);
|
|
9941
|
+
return allow4(`Policy engine check failed: ${message}`, "policy-engine", [
|
|
9942
|
+
"policy-engine-error"
|
|
9943
|
+
]);
|
|
9944
|
+
}
|
|
9652
9945
|
}
|
|
9653
|
-
|
|
9654
|
-
|
|
9655
|
-
|
|
9656
|
-
|
|
9657
|
-
|
|
9658
|
-
|
|
9659
|
-
|
|
9660
|
-
|
|
9661
|
-
|
|
9662
|
-
|
|
9663
|
-
|
|
9664
|
-
|
|
9946
|
+
function checkGovernance(input) {
|
|
9947
|
+
try {
|
|
9948
|
+
const riskFlags = [];
|
|
9949
|
+
if (config.governance?.toolGuard !== false) {
|
|
9950
|
+
const tg = evaluate(input);
|
|
9951
|
+
if (tg.verdict === "deny")
|
|
9952
|
+
return tg;
|
|
9953
|
+
riskFlags.push(...tg.riskFlags);
|
|
9954
|
+
}
|
|
9955
|
+
if (config.governance?.guardRails !== false) {
|
|
9956
|
+
const gr = evaluate2(input);
|
|
9957
|
+
if (gr.verdict === "deny")
|
|
9958
|
+
return gr;
|
|
9959
|
+
riskFlags.push(...gr.riskFlags);
|
|
9960
|
+
}
|
|
9961
|
+
if (config.governance?.approvals !== false) {
|
|
9962
|
+
const ap = evaluate3(input);
|
|
9963
|
+
if (ap.verdict !== "allow")
|
|
9964
|
+
return ap;
|
|
9965
|
+
}
|
|
9966
|
+
const pe = evaluatePolicyEngine(input);
|
|
9967
|
+
if (pe.verdict === "deny")
|
|
9968
|
+
return pe;
|
|
9969
|
+
riskFlags.push(...pe.riskFlags);
|
|
9970
|
+
return allow4("Governance checks passed", "governance", riskFlags);
|
|
9971
|
+
} catch (error) {
|
|
9972
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
9973
|
+
if (appLog)
|
|
9974
|
+
appLog(`[harness-policy] governance error: ${message}`);
|
|
9975
|
+
return deny3(`Governance check failed: ${message}`, "governance", `Governance check failed: ${message}`, ["governance-error"]);
|
|
9976
|
+
}
|
|
9665
9977
|
}
|
|
9666
|
-
|
|
9667
|
-
|
|
9668
|
-
|
|
9978
|
+
function checkSupervisor(input, _targetName) {
|
|
9979
|
+
if (config.governance?.supervisor?.enabled === false) {
|
|
9980
|
+
return allow4("Supervisor disabled", "supervisor");
|
|
9981
|
+
}
|
|
9982
|
+
try {
|
|
9983
|
+
return reviewToolCall(input.directory, input);
|
|
9984
|
+
} catch (error) {
|
|
9985
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
9986
|
+
if (appLog)
|
|
9987
|
+
appLog(`[harness-policy] supervisor error: ${message}`);
|
|
9988
|
+
return deny3(`Supervisor review failed: ${message}`, "supervisor", `Supervisor review failed: ${message}`, ["supervisor-error"]);
|
|
9989
|
+
}
|
|
9669
9990
|
}
|
|
9670
|
-
|
|
9671
|
-
|
|
9672
|
-
|
|
9673
|
-
|
|
9674
|
-
|
|
9675
|
-
|
|
9676
|
-
|
|
9677
|
-
|
|
9678
|
-
|
|
9679
|
-
}
|
|
9680
|
-
function _ambiguous(signals, prompt) {
|
|
9681
|
-
return {
|
|
9682
|
-
taskType: "ambiguous",
|
|
9683
|
-
confidence: 0,
|
|
9684
|
-
signals,
|
|
9685
|
-
requiresDesign: false,
|
|
9686
|
-
requiresTDD: false,
|
|
9687
|
-
stageSequence: [],
|
|
9688
|
-
clarificationNeeded: true,
|
|
9689
|
-
clarificationPrompt: prompt
|
|
9690
|
-
};
|
|
9691
|
-
}
|
|
9692
|
-
function buildStageSequence(taskType) {
|
|
9693
|
-
switch (taskType) {
|
|
9694
|
-
case "feature":
|
|
9695
|
-
return [
|
|
9696
|
-
stage2("discuss", "fd-discuss", false, false),
|
|
9697
|
-
stage2("plan", "fd-plan", true, false),
|
|
9698
|
-
stage2("execute", "fd-execute", false, false),
|
|
9699
|
-
stage2("verify", "fd-verify", false, false)
|
|
9700
|
-
];
|
|
9701
|
-
case "ui-feature":
|
|
9702
|
-
return [
|
|
9703
|
-
stage2("discuss", "fd-discuss", false, false),
|
|
9704
|
-
stage2("design", "fd-design", false, false, "--mode=draft"),
|
|
9705
|
-
stage2("plan", "fd-plan", true, false),
|
|
9706
|
-
stage2("execute", "fd-execute", false, false),
|
|
9707
|
-
stage2("verify", "fd-verify", false, false)
|
|
9708
|
-
];
|
|
9709
|
-
case "bugfix":
|
|
9710
|
-
return [
|
|
9711
|
-
stage2("discuss", "fd-discuss", false, false),
|
|
9712
|
-
stage2("fix-bug", "fd-fix-bug", false, false),
|
|
9713
|
-
stage2("verify", "fd-verify", false, false)
|
|
9714
|
-
];
|
|
9715
|
-
case "docs":
|
|
9716
|
-
return [
|
|
9717
|
-
stage2("discuss", "fd-discuss", false, false),
|
|
9718
|
-
stage2("write-docs", "fd-write-docs", false, false),
|
|
9719
|
-
stage2("verify", "fd-verify", false, true)
|
|
9720
|
-
];
|
|
9721
|
-
case "simple":
|
|
9722
|
-
return [
|
|
9723
|
-
stage2("execute", "fd-execute", false, false),
|
|
9724
|
-
stage2("verify", "fd-verify", false, true)
|
|
9725
|
-
];
|
|
9726
|
-
case "ambiguous":
|
|
9727
|
-
return [];
|
|
9728
|
-
default:
|
|
9729
|
-
return [];
|
|
9991
|
+
function generateApprovalRequestId(input) {
|
|
9992
|
+
try {
|
|
9993
|
+
const req = requestApprovalForTool(input.directory, input.runId, input.sessionID, input.agent, input.tool, input.args);
|
|
9994
|
+
return req.id;
|
|
9995
|
+
} catch (error) {
|
|
9996
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
9997
|
+
if (appLog)
|
|
9998
|
+
appLog(`[harness-policy] approval request failed: ${message}`);
|
|
9999
|
+
return;
|
|
10000
|
+
}
|
|
9730
10001
|
}
|
|
9731
|
-
|
|
9732
|
-
|
|
9733
|
-
|
|
9734
|
-
|
|
9735
|
-
|
|
9736
|
-
|
|
9737
|
-
|
|
9738
|
-
|
|
9739
|
-
|
|
9740
|
-
|
|
9741
|
-
|
|
9742
|
-
|
|
9743
|
-
|
|
9744
|
-
|
|
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;
|
|
10002
|
+
function combineDecisions(decisions, input) {
|
|
10003
|
+
const denied = decisions.find((d) => d.verdict === "deny");
|
|
10004
|
+
if (denied)
|
|
10005
|
+
return denied;
|
|
10006
|
+
const asked = decisions.find((d) => d.verdict === "ask");
|
|
10007
|
+
if (asked) {
|
|
10008
|
+
return {
|
|
10009
|
+
...asked,
|
|
10010
|
+
approvalRequestId: asked.approvalRequestId ?? generateApprovalRequestId(input)
|
|
10011
|
+
};
|
|
10012
|
+
}
|
|
10013
|
+
const riskFlags = decisions.flatMap((d) => d.riskFlags);
|
|
10014
|
+
const source = decisions.findLast((d) => d.source !== "harness.policy")?.source ?? "harness.policy";
|
|
10015
|
+
return allow4("All policy checks passed", source, riskFlags);
|
|
9759
10016
|
}
|
|
9760
|
-
|
|
9761
|
-
|
|
9762
|
-
|
|
9763
|
-
|
|
9764
|
-
|
|
9765
|
-
|
|
9766
|
-
|
|
9767
|
-
|
|
9768
|
-
|
|
9769
|
-
|
|
9770
|
-
|
|
10017
|
+
function evaluate4(input) {
|
|
10018
|
+
if (config.harness?.enabled === false) {
|
|
10019
|
+
const envDisabled = process.env.FLOWDECK_HARNESS_DISABLED === "1";
|
|
10020
|
+
if (!envDisabled) {
|
|
10021
|
+
if (appLog) {
|
|
10022
|
+
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).");
|
|
10023
|
+
}
|
|
10024
|
+
const runtime2 = checkRuntimeLimits(input);
|
|
10025
|
+
if (runtime2.verdict === "deny") {
|
|
10026
|
+
appendAudit(input, runtime2);
|
|
10027
|
+
return runtime2;
|
|
10028
|
+
}
|
|
10029
|
+
if (config.governance?.toolGuard !== false) {
|
|
10030
|
+
const tg = evaluate(input);
|
|
10031
|
+
if (tg.verdict === "deny") {
|
|
10032
|
+
appendAudit(input, tg);
|
|
10033
|
+
return tg;
|
|
10034
|
+
}
|
|
10035
|
+
}
|
|
10036
|
+
const decision2 = allow4("Harness disabled — minimal safety checks passed", "harness.policy", ["harness-disabled-minimal"]);
|
|
10037
|
+
appendAudit(input, decision2);
|
|
10038
|
+
return decision2;
|
|
10039
|
+
}
|
|
10040
|
+
const decision = allow4("Harness disabled by config and env override", "harness.policy");
|
|
10041
|
+
appendAudit(input, decision);
|
|
10042
|
+
return decision;
|
|
10043
|
+
}
|
|
10044
|
+
const decisions = [];
|
|
10045
|
+
const runtime = checkRuntimeLimits(input);
|
|
10046
|
+
decisions.push(runtime);
|
|
10047
|
+
if (runtime.verdict === "deny") {
|
|
10048
|
+
appendAudit(input, runtime);
|
|
10049
|
+
return runtime;
|
|
10050
|
+
}
|
|
10051
|
+
const contract = checkAgentContract(input);
|
|
10052
|
+
decisions.push(contract);
|
|
10053
|
+
if (contract.verdict === "deny") {
|
|
10054
|
+
appendAudit(input, contract);
|
|
10055
|
+
return contract;
|
|
10056
|
+
}
|
|
10057
|
+
const governance = checkGovernance(input);
|
|
10058
|
+
decisions.push(governance);
|
|
10059
|
+
if (governance.verdict === "deny") {
|
|
10060
|
+
appendAudit(input, governance);
|
|
10061
|
+
return governance;
|
|
10062
|
+
}
|
|
10063
|
+
const supervisor = checkSupervisor(input, input.agent);
|
|
10064
|
+
decisions.push(supervisor);
|
|
10065
|
+
const final = combineDecisions(decisions, input);
|
|
10066
|
+
appendAudit(input, final);
|
|
10067
|
+
return final;
|
|
9771
10068
|
}
|
|
9772
|
-
const enrichedPrompt = refinement.supervisorContext ? `${base.clarificationPrompt ?? ""} (Context: ${refinement.supervisorContext})` : base.clarificationPrompt;
|
|
9773
10069
|
return {
|
|
9774
|
-
|
|
9775
|
-
|
|
10070
|
+
evaluate: evaluate4,
|
|
10071
|
+
checkAgentContract,
|
|
10072
|
+
checkRuntimeLimits,
|
|
10073
|
+
checkGovernance,
|
|
10074
|
+
checkSupervisor
|
|
9776
10075
|
};
|
|
9777
10076
|
}
|
|
9778
10077
|
|
|
10078
|
+
// src/services/harness-controller.ts
|
|
10079
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
10080
|
+
|
|
9779
10081
|
// src/services/run-trace.ts
|
|
9780
10082
|
import { existsSync as existsSync26, readFileSync as readFileSync26, appendFileSync as appendFileSync7, writeFileSync as writeFileSync16, mkdirSync as mkdirSync16 } from "fs";
|
|
9781
10083
|
import { join as join26 } from "path";
|
|
@@ -10468,28 +10770,33 @@ var DISABLED = process.env.FLOWDECK_ORCHESTRATOR_GUARD === "off";
|
|
|
10468
10770
|
function normalizeToolName(name) {
|
|
10469
10771
|
return name.toLowerCase().replace(/[-_]/g, "");
|
|
10470
10772
|
}
|
|
10773
|
+
function buildRoutingOptions() {
|
|
10774
|
+
return getAllContracts().filter((c) => c.agent !== "orchestrator").map((c) => ` @${c.agent.padEnd(20)} — ${c.role}`).join(`
|
|
10775
|
+
`);
|
|
10776
|
+
}
|
|
10471
10777
|
function blockMessage(toolName) {
|
|
10472
10778
|
const contract = getContract("orchestrator");
|
|
10473
10779
|
const allowed = contract?.allowedTools ?? [];
|
|
10474
|
-
return
|
|
10475
|
-
|
|
10476
|
-
|
|
10477
|
-
|
|
10478
|
-
|
|
10479
|
-
|
|
10480
|
-
|
|
10481
|
-
|
|
10482
|
-
|
|
10483
|
-
|
|
10484
|
-
|
|
10485
|
-
|
|
10486
|
-
|
|
10487
|
-
|
|
10488
|
-
|
|
10489
|
-
|
|
10490
|
-
|
|
10491
|
-
|
|
10492
|
-
`
|
|
10780
|
+
return [
|
|
10781
|
+
`[Orchestrator Guard] \`${toolName}\` is not in the orchestrator's allowed tools.`,
|
|
10782
|
+
"",
|
|
10783
|
+
"The orchestrator coordinates — it does not execute directly.",
|
|
10784
|
+
"To route work, mention the agent inline with FULL task context:",
|
|
10785
|
+
"",
|
|
10786
|
+
" @backend-coder",
|
|
10787
|
+
" Task: <exact task description>",
|
|
10788
|
+
" Files: <file targets>",
|
|
10789
|
+
" Constraints: <constraints>",
|
|
10790
|
+
" Acceptance criteria: <what done looks like>",
|
|
10791
|
+
"",
|
|
10792
|
+
"Available agents:",
|
|
10793
|
+
buildRoutingOptions(),
|
|
10794
|
+
"",
|
|
10795
|
+
`Orchestrator allowed tools: ${allowed.filter((t) => t !== "delegate").join(", ")}.`,
|
|
10796
|
+
"",
|
|
10797
|
+
"To disable this guard: set FLOWDECK_ORCHESTRATOR_GUARD=off"
|
|
10798
|
+
].join(`
|
|
10799
|
+
`);
|
|
10493
10800
|
}
|
|
10494
10801
|
|
|
10495
10802
|
class OrchestratorGuard {
|
|
@@ -10520,21 +10827,20 @@ class OrchestratorGuard {
|
|
|
10520
10827
|
}
|
|
10521
10828
|
check(sessionId, toolName) {
|
|
10522
10829
|
if (DISABLED)
|
|
10523
|
-
return;
|
|
10830
|
+
return { allowed: true };
|
|
10524
10831
|
if (this.primarySessionId === null)
|
|
10525
|
-
return;
|
|
10832
|
+
return { allowed: true };
|
|
10526
10833
|
if (sessionId !== this.primarySessionId)
|
|
10527
|
-
return;
|
|
10834
|
+
return { allowed: true };
|
|
10528
10835
|
const contract = getContract("orchestrator");
|
|
10529
10836
|
if (!contract) {
|
|
10530
|
-
|
|
10837
|
+
return { allowed: false, message: blockMessage(toolName) };
|
|
10531
10838
|
}
|
|
10532
10839
|
const normalizedTool = normalizeToolName(toolName);
|
|
10533
|
-
const
|
|
10534
|
-
if (
|
|
10535
|
-
return;
|
|
10536
|
-
|
|
10537
|
-
throw new Error(blockMessage(toolName));
|
|
10840
|
+
const isAllowed = contract.allowedTools.some((t) => normalizeToolName(t) === normalizedTool);
|
|
10841
|
+
if (isAllowed)
|
|
10842
|
+
return { allowed: true };
|
|
10843
|
+
return { allowed: false, message: blockMessage(toolName) };
|
|
10538
10844
|
}
|
|
10539
10845
|
_isBlockedForTest(name) {
|
|
10540
10846
|
const contract = getContract("orchestrator");
|
|
@@ -11022,6 +11328,7 @@ function createHarnessController(config) {
|
|
|
11022
11328
|
const maxToolCalls = delegationConfig?.maxToolCalls ?? 200;
|
|
11023
11329
|
const sessionDepthMap = new Map;
|
|
11024
11330
|
const blockedSessions = new Set;
|
|
11331
|
+
const pendingDepthBlockNotifications = new Map;
|
|
11025
11332
|
const state = {
|
|
11026
11333
|
loopDetector,
|
|
11027
11334
|
eventLog,
|
|
@@ -11059,7 +11366,8 @@ function createHarnessController(config) {
|
|
|
11059
11366
|
projectRoot: directory,
|
|
11060
11367
|
command,
|
|
11061
11368
|
description,
|
|
11062
|
-
currentStage
|
|
11369
|
+
currentStage,
|
|
11370
|
+
workflowDecision: route
|
|
11063
11371
|
});
|
|
11064
11372
|
const budget = assembledContext.tokenBudget;
|
|
11065
11373
|
log(`[context-ingress] run=${sessionID} trivial=${assembledContext.isTrivialChat} ` + `tokens=${budget.usedTokens}/${budget.limitTokens} (${budget.component})`);
|
|
@@ -11162,16 +11470,14 @@ function createHarnessController(config) {
|
|
|
11162
11470
|
});
|
|
11163
11471
|
state.lastActiveSessionID = req.sessionID;
|
|
11164
11472
|
const agent = req.agent ?? "orchestrator";
|
|
11165
|
-
|
|
11166
|
-
|
|
11167
|
-
} catch (error) {
|
|
11168
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
11473
|
+
const guardResult = orchestratorGuard.check(req.sessionID, req.tool);
|
|
11474
|
+
if (!guardResult.allowed) {
|
|
11169
11475
|
return {
|
|
11170
11476
|
verdict: "deny",
|
|
11171
|
-
reason: message,
|
|
11477
|
+
reason: guardResult.message,
|
|
11172
11478
|
riskFlags: ["orchestrator-contract"],
|
|
11173
11479
|
source: "orchestrator-guard",
|
|
11174
|
-
escalationMessage: message
|
|
11480
|
+
escalationMessage: guardResult.message
|
|
11175
11481
|
};
|
|
11176
11482
|
}
|
|
11177
11483
|
executionSubstrate.start({
|
|
@@ -11236,6 +11542,9 @@ function createHarnessController(config) {
|
|
|
11236
11542
|
if (childDepth > maxDepth) {
|
|
11237
11543
|
log(`[harness] delegation depth ${childDepth} exceeds maxDepth ${maxDepth} — session ${sessionID} will be policy-blocked`);
|
|
11238
11544
|
blockedSessions.add(sessionID);
|
|
11545
|
+
if (parentSessionID) {
|
|
11546
|
+
pendingDepthBlockNotifications.set(sessionID, parentSessionID);
|
|
11547
|
+
}
|
|
11239
11548
|
}
|
|
11240
11549
|
} else {
|
|
11241
11550
|
sessionDepthMap.set(sessionID, 0);
|
|
@@ -11244,6 +11553,7 @@ function createHarnessController(config) {
|
|
|
11244
11553
|
if ((type === "session.idle" || type === "session.error") && sessionID) {
|
|
11245
11554
|
sessionDepthMap.delete(sessionID);
|
|
11246
11555
|
blockedSessions.delete(sessionID);
|
|
11556
|
+
pendingDepthBlockNotifications.delete(sessionID);
|
|
11247
11557
|
}
|
|
11248
11558
|
orchestratorGuard.onEvent({ type, properties });
|
|
11249
11559
|
if (type === "session.created" || type === "session.started") {
|
|
@@ -11288,6 +11598,14 @@ function createHarnessController(config) {
|
|
|
11288
11598
|
},
|
|
11289
11599
|
getContextMonitor() {
|
|
11290
11600
|
return contextMonitor.get();
|
|
11601
|
+
},
|
|
11602
|
+
getPendingDepthBlockNotifications() {
|
|
11603
|
+
const notifications = [];
|
|
11604
|
+
for (const [sessionID, parentSessionID] of pendingDepthBlockNotifications) {
|
|
11605
|
+
notifications.push({ sessionID, parentSessionID });
|
|
11606
|
+
}
|
|
11607
|
+
pendingDepthBlockNotifications.clear();
|
|
11608
|
+
return notifications;
|
|
11291
11609
|
}
|
|
11292
11610
|
};
|
|
11293
11611
|
}
|
|
@@ -11503,6 +11821,9 @@ var plugin = async (input, _options) => {
|
|
|
11503
11821
|
type: event.type,
|
|
11504
11822
|
properties: event.properties
|
|
11505
11823
|
});
|
|
11824
|
+
for (const { sessionID: blockedSessionID, parentSessionID } of harness.getPendingDepthBlockNotifications()) {
|
|
11825
|
+
notify("FlowDeck: Delegation depth limit reached", `Session ${blockedSessionID} (parent ${parentSessionID}) exceeded max delegation depth. Stop further delegation from this subagent.`, "critical");
|
|
11826
|
+
}
|
|
11506
11827
|
const type = event?.type ?? "";
|
|
11507
11828
|
if (type === "command.executed") {
|
|
11508
11829
|
const commandName = String(event?.properties?.name ?? "");
|
|
@@ -11533,6 +11854,11 @@ var plugin = async (input, _options) => {
|
|
|
11533
11854
|
},
|
|
11534
11855
|
"tool.execute.before": async (toolInput, toolOutput) => {
|
|
11535
11856
|
const sessionID = toolInput.sessionID ?? "";
|
|
11857
|
+
harness.ensureRunContext({
|
|
11858
|
+
sessionID,
|
|
11859
|
+
description: `${toolInput.tool ?? toolInput.name ?? "unknown"} ${JSON.stringify(toolOutput?.args ?? toolInput?.args ?? {})} auto-invoked`,
|
|
11860
|
+
agent: toolInput.agent
|
|
11861
|
+
});
|
|
11536
11862
|
const budgetCheck = harness.checkToolCallBudget(sessionID);
|
|
11537
11863
|
if (!budgetCheck.allow) {
|
|
11538
11864
|
throw new Error(budgetCheck.reason ?? `Tool blocked: ${budgetCheck.reason}`);
|