@dv.nghiem/flowdeck 0.4.4 → 0.4.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/index.d.ts.map +1 -1
- package/dist/agents/risk-analyst.d.ts.map +1 -1
- package/dist/dashboard/lib/state-reader.d.ts.map +1 -1
- package/dist/dashboard/server.mjs +22 -73
- package/dist/hooks/approval-hook.d.ts.map +1 -1
- package/dist/hooks/event-log-hook.d.ts +30 -0
- package/dist/hooks/event-log-hook.d.ts.map +1 -0
- package/dist/hooks/notifications.d.ts.map +1 -1
- package/dist/hooks/orchestrator-guard-hook.d.ts.map +1 -1
- package/dist/hooks/patch-trust.d.ts +0 -1
- package/dist/hooks/patch-trust.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +627 -1159
- package/dist/lib/impact-radar.d.ts.map +1 -1
- package/dist/lib/research-gate.d.ts +6 -2
- package/dist/lib/research-gate.d.ts.map +1 -1
- package/dist/services/agent-validator.d.ts +1 -1
- package/dist/services/agent-validator.d.ts.map +1 -1
- package/dist/services/event-logger.d.ts +18 -0
- package/dist/services/event-logger.d.ts.map +1 -0
- package/dist/services/supervisor-binding.d.ts.map +1 -1
- package/dist/services/token-metrics.d.ts +1 -1
- package/dist/services/workflow-scorecard.d.ts.map +1 -1
- package/dist/tools/delegate.d.ts +1 -2
- package/dist/tools/delegate.d.ts.map +1 -1
- package/dist/tools/run-pipeline.d.ts +1 -2
- package/dist/tools/run-pipeline.d.ts.map +1 -1
- package/docs/commands/fd-deploy-check.md +1 -5
- package/docs/commands/fd-reflect.md +8 -9
- package/docs/commands/fd-suggest.md +3 -4
- package/docs/concepts/architecture.md +0 -3
- package/docs/concepts/intelligence.md +2 -36
- package/docs/skills/index.md +0 -2
- package/package.json +2 -2
- package/src/commands/fd-deploy-check.md +1 -5
- package/src/commands/fd-reflect.md +4 -6
- package/src/commands/fd-suggest.md +3 -4
- package/src/skills/change-impact-radar/SKILL.md +3 -4
- package/src/skills/confidence-aware-planning/SKILL.md +0 -1
- package/src/skills/patch-trust-score/SKILL.md +3 -5
- package/dist/dashboard/lib/port-finder.test.d.ts +0 -2
- package/dist/dashboard/lib/port-finder.test.d.ts.map +0 -1
- package/dist/hooks/notifications.test.d.ts +0 -14
- package/dist/hooks/notifications.test.d.ts.map +0 -1
- package/dist/hooks/patch-trust.test.d.ts +0 -2
- package/dist/hooks/patch-trust.test.d.ts.map +0 -1
- package/dist/hooks/telemetry-hook.d.ts +0 -33
- package/dist/hooks/telemetry-hook.d.ts.map +0 -1
- package/dist/hooks/telemetry-hook.test.d.ts +0 -2
- package/dist/hooks/telemetry-hook.test.d.ts.map +0 -1
- package/dist/hooks/tool-guard.test.d.ts +0 -2
- package/dist/hooks/tool-guard.test.d.ts.map +0 -1
- package/dist/lib/research-gate.test.d.ts +0 -2
- package/dist/lib/research-gate.test.d.ts.map +0 -1
- package/dist/services/activity-reporter.d.ts +0 -96
- package/dist/services/activity-reporter.d.ts.map +0 -1
- package/dist/services/activity-reporter.test.d.ts +0 -2
- package/dist/services/activity-reporter.test.d.ts.map +0 -1
- package/dist/services/artifact-store.d.ts +0 -39
- package/dist/services/artifact-store.d.ts.map +0 -1
- package/dist/services/artifact-store.test.d.ts +0 -2
- package/dist/services/artifact-store.test.d.ts.map +0 -1
- package/dist/services/codegraph.test.d.ts +0 -2
- package/dist/services/codegraph.test.d.ts.map +0 -1
- package/dist/services/command-validator.test.d.ts +0 -2
- package/dist/services/command-validator.test.d.ts.map +0 -1
- package/dist/services/context-assembler.d.ts +0 -29
- package/dist/services/context-assembler.d.ts.map +0 -1
- package/dist/services/context-assembler.test.d.ts +0 -2
- package/dist/services/context-assembler.test.d.ts.map +0 -1
- package/dist/services/cost-budget.d.ts +0 -53
- package/dist/services/cost-budget.d.ts.map +0 -1
- package/dist/services/cost-budget.test.d.ts +0 -2
- package/dist/services/cost-budget.test.d.ts.map +0 -1
- package/dist/services/cost-estimator.test.d.ts +0 -2
- package/dist/services/cost-estimator.test.d.ts.map +0 -1
- package/dist/services/draft-verifier.d.ts +0 -48
- package/dist/services/draft-verifier.d.ts.map +0 -1
- package/dist/services/draft-verifier.test.d.ts +0 -2
- package/dist/services/draft-verifier.test.d.ts.map +0 -1
- package/dist/services/governance.test.d.ts +0 -11
- package/dist/services/governance.test.d.ts.map +0 -1
- package/dist/services/index.d.ts +0 -25
- package/dist/services/index.d.ts.map +0 -1
- package/dist/services/lazy-rule-loader.test.d.ts +0 -23
- package/dist/services/lazy-rule-loader.test.d.ts.map +0 -1
- package/dist/services/model-router-ext.test.d.ts +0 -2
- package/dist/services/model-router-ext.test.d.ts.map +0 -1
- package/dist/services/model-router.test.d.ts +0 -2
- package/dist/services/model-router.test.d.ts.map +0 -1
- package/dist/services/policy-compiler.d.ts +0 -27
- package/dist/services/policy-compiler.d.ts.map +0 -1
- package/dist/services/preflight-explorer.test.d.ts +0 -25
- package/dist/services/preflight-explorer.test.d.ts.map +0 -1
- package/dist/services/prompt-cache-ext.test.d.ts +0 -2
- package/dist/services/prompt-cache-ext.test.d.ts.map +0 -1
- package/dist/services/prompt-cache.test.d.ts +0 -2
- package/dist/services/prompt-cache.test.d.ts.map +0 -1
- package/dist/services/quick-router.test.d.ts +0 -13
- package/dist/services/quick-router.test.d.ts.map +0 -1
- package/dist/services/recommended-question.test.d.ts +0 -2
- package/dist/services/recommended-question.test.d.ts.map +0 -1
- package/dist/services/rtk-manager.test.d.ts +0 -2
- package/dist/services/rtk-manager.test.d.ts.map +0 -1
- package/dist/services/rtk-policy.test.d.ts +0 -2
- package/dist/services/rtk-policy.test.d.ts.map +0 -1
- package/dist/services/rule-engine.d.ts +0 -29
- package/dist/services/rule-engine.d.ts.map +0 -1
- package/dist/services/rule-engine.test.d.ts +0 -2
- package/dist/services/rule-engine.test.d.ts.map +0 -1
- package/dist/services/services.test.d.ts +0 -2
- package/dist/services/services.test.d.ts.map +0 -1
- package/dist/services/supervisor.test.d.ts +0 -14
- package/dist/services/supervisor.test.d.ts.map +0 -1
- package/dist/services/task-batcher.d.ts +0 -48
- package/dist/services/task-batcher.d.ts.map +0 -1
- package/dist/services/task-batcher.test.d.ts +0 -2
- package/dist/services/task-batcher.test.d.ts.map +0 -1
- package/dist/services/telemetry.d.ts +0 -48
- package/dist/services/telemetry.d.ts.map +0 -1
- package/dist/services/token-budget.d.ts +0 -44
- package/dist/services/token-budget.d.ts.map +0 -1
- package/dist/services/token-budget.test.d.ts +0 -2
- package/dist/services/token-budget.test.d.ts.map +0 -1
- package/dist/services/token-metrics-ext.test.d.ts +0 -2
- package/dist/services/token-metrics-ext.test.d.ts.map +0 -1
- package/dist/services/token-metrics.test.d.ts +0 -2
- package/dist/services/token-metrics.test.d.ts.map +0 -1
- package/dist/tools/agent-dispatch.test.d.ts +0 -2
- package/dist/tools/agent-dispatch.test.d.ts.map +0 -1
- package/dist/tools/codebase-index.test.d.ts +0 -2
- package/dist/tools/codebase-index.test.d.ts.map +0 -1
- package/dist/tools/context-generator.d.ts +0 -3
- package/dist/tools/context-generator.d.ts.map +0 -1
- package/dist/tools/create-skill.d.ts +0 -3
- package/dist/tools/create-skill.d.ts.map +0 -1
- package/dist/tools/dispatch-routing.test.d.ts +0 -2
- package/dist/tools/dispatch-routing.test.d.ts.map +0 -1
- package/dist/tools/failure-replay.test.d.ts +0 -2
- package/dist/tools/failure-replay.test.d.ts.map +0 -1
- package/dist/tools/repo-memory.test.d.ts +0 -2
- package/dist/tools/repo-memory.test.d.ts.map +0 -1
- package/dist/tools/volatility-map.d.ts +0 -18
- package/dist/tools/volatility-map.d.ts.map +0 -1
- package/dist/tools/volatility-map.test.d.ts +0 -2
- package/dist/tools/volatility-map.test.d.ts.map +0 -1
- package/dist/tools/workspace-state.d.ts +0 -3
- package/dist/tools/workspace-state.d.ts.map +0 -1
- package/src/skills/volatility-map/SKILL.md +0 -52
package/dist/index.js
CHANGED
|
@@ -2,10 +2,10 @@ import { createRequire } from "node:module";
|
|
|
2
2
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
3
3
|
|
|
4
4
|
// src/index.ts
|
|
5
|
-
import { readFileSync as
|
|
6
|
-
import { join as
|
|
7
|
-
import { dirname as
|
|
8
|
-
import { fileURLToPath as
|
|
5
|
+
import { readFileSync as readFileSync28, readdirSync as readdirSync4, existsSync as existsSync29 } from "fs";
|
|
6
|
+
import { join as join27, basename as basename2 } from "path";
|
|
7
|
+
import { dirname as dirname3 } from "path";
|
|
8
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
9
9
|
|
|
10
10
|
// src/services/lazy-rule-loader.ts
|
|
11
11
|
import { existsSync, readFileSync, readdirSync } from "fs";
|
|
@@ -393,14 +393,6 @@ function findWorkspaceRoot(startDir) {
|
|
|
393
393
|
}
|
|
394
394
|
return null;
|
|
395
395
|
}
|
|
396
|
-
function resolveSubRepos(configPath, subRepos) {
|
|
397
|
-
const configDir = dirname(configPath);
|
|
398
|
-
return subRepos.map((r) => {
|
|
399
|
-
if (resolve(r) === r)
|
|
400
|
-
return r;
|
|
401
|
-
return resolve(configDir, r);
|
|
402
|
-
});
|
|
403
|
-
}
|
|
404
396
|
function getWorkspaceConfig(dir) {
|
|
405
397
|
const root = findWorkspaceRoot(dir);
|
|
406
398
|
if (!root)
|
|
@@ -577,173 +569,30 @@ ${entry}`, "utf-8");
|
|
|
577
569
|
}
|
|
578
570
|
});
|
|
579
571
|
|
|
580
|
-
// src/tools/workspace-state.ts
|
|
581
|
-
import { tool as tool3 } from "@opencode-ai/plugin";
|
|
582
|
-
import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, existsSync as existsSync5 } from "fs";
|
|
583
|
-
import { join as join5 } from "path";
|
|
584
|
-
function getRepoName(repoPath) {
|
|
585
|
-
return repoPath.split(/[/\\]/).pop() || repoPath;
|
|
586
|
-
}
|
|
587
|
-
function readWorkspaceStateFile(workspaceRoot) {
|
|
588
|
-
const sp = join5(planningDir(workspaceRoot), "STATE.md");
|
|
589
|
-
if (!existsSync5(sp))
|
|
590
|
-
return null;
|
|
591
|
-
return readFileSync5(sp, "utf-8");
|
|
592
|
-
}
|
|
593
|
-
function writeWorkspaceStateFile(workspaceRoot, content) {
|
|
594
|
-
const sp = join5(planningDir(workspaceRoot), "STATE.md");
|
|
595
|
-
writeFileSync4(sp, content, "utf-8");
|
|
596
|
-
}
|
|
597
|
-
function parseWorkspaceState(content) {
|
|
598
|
-
const result = {};
|
|
599
|
-
const lines = content.split(`
|
|
600
|
-
`);
|
|
601
|
-
for (const line of lines) {
|
|
602
|
-
const kvMatch = line.match(/^\*\*([^:]+):\*\*\s*(.+)/);
|
|
603
|
-
if (kvMatch) {
|
|
604
|
-
result[kvMatch[1].trim()] = kvMatch[2].trim();
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
return result;
|
|
608
|
-
}
|
|
609
|
-
async function readWorkspaceContextAction(dir, mode, workspaceRoot) {
|
|
610
|
-
const stateContent = readWorkspaceStateFile(workspaceRoot);
|
|
611
|
-
if (!stateContent) {
|
|
612
|
-
return { error: "Workspace STATE.md not found. Initialize workspace first." };
|
|
613
|
-
}
|
|
614
|
-
const parsed = parseWorkspaceState(stateContent);
|
|
615
|
-
return { exists: true, workspace_root: workspaceRoot, workspace_mode: mode, ...parsed };
|
|
616
|
-
}
|
|
617
|
-
async function updateWorkspaceContextAction(dir, mode, workspaceRoot, updates) {
|
|
618
|
-
if (!updates)
|
|
619
|
-
return { error: "No updates provided" };
|
|
620
|
-
let content = readWorkspaceStateFile(workspaceRoot);
|
|
621
|
-
if (!content) {
|
|
622
|
-
content = `# Workspace State
|
|
623
|
-
|
|
624
|
-
---
|
|
625
|
-
|
|
626
|
-
`;
|
|
627
|
-
}
|
|
628
|
-
if (updates.current_repo !== undefined) {
|
|
629
|
-
const regex = /^\*\*current_repo:\*\*.*/m;
|
|
630
|
-
if (regex.test(content)) {
|
|
631
|
-
content = content.replace(regex, `**current_repo:** ${updates.current_repo}`);
|
|
632
|
-
} else {
|
|
633
|
-
content = content.replace(/^---\n/, `---
|
|
634
|
-
|
|
635
|
-
**current_repo:** ${updates.current_repo}
|
|
636
|
-
`);
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
if (updates.status !== undefined) {
|
|
640
|
-
const regex = /^\*\*status:\*\*.*/m;
|
|
641
|
-
if (regex.test(content)) {
|
|
642
|
-
content = content.replace(regex, `**status:** ${updates.status}`);
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
writeWorkspaceStateFile(workspaceRoot, content);
|
|
646
|
-
return { success: true, updated_at: timestamp() };
|
|
647
|
-
}
|
|
648
|
-
async function listSubReposAction(dir, subRepos, workspaceRoot) {
|
|
649
|
-
const resolved = resolveSubRepos(join5(workspaceRoot, ".planning", "config.json"), subRepos);
|
|
650
|
-
const repos = [];
|
|
651
|
-
for (const repoPath of resolved) {
|
|
652
|
-
const repoName = getRepoName(repoPath);
|
|
653
|
-
const planningPath = planningDir(repoPath);
|
|
654
|
-
const hasPlanning = existsSync5(planningPath);
|
|
655
|
-
repos.push({
|
|
656
|
-
name: repoName,
|
|
657
|
-
path: repoPath,
|
|
658
|
-
status: hasPlanning ? "active" : "not_initialized"
|
|
659
|
-
});
|
|
660
|
-
}
|
|
661
|
-
return { repos };
|
|
662
|
-
}
|
|
663
|
-
async function getSubRepoStateAction(dir, repoName, subRepos, workspaceRoot, mode) {
|
|
664
|
-
if (!repoName)
|
|
665
|
-
return { error: "repo name is required" };
|
|
666
|
-
const resolved = resolveSubRepos(join5(workspaceRoot, ".planning", "config.json"), subRepos);
|
|
667
|
-
const targetPath = resolved.find((p) => getRepoName(p) === repoName);
|
|
668
|
-
if (!targetPath) {
|
|
669
|
-
return { error: "not_found", path: repoName, message: `Repo '${repoName}' not found in sub_repos` };
|
|
670
|
-
}
|
|
671
|
-
const planningPath = planningDir(targetPath);
|
|
672
|
-
if (!existsSync5(planningPath)) {
|
|
673
|
-
return { error: "not_found", path: targetPath };
|
|
674
|
-
}
|
|
675
|
-
const sp = statePath(targetPath);
|
|
676
|
-
if (!existsSync5(sp)) {
|
|
677
|
-
return { error: "not_found", path: targetPath, message: `.planning/STATE.md not found in ${repoName}` };
|
|
678
|
-
}
|
|
679
|
-
const stateContent = readFileSync5(sp, "utf-8");
|
|
680
|
-
const parsed = parseWorkspaceState(stateContent);
|
|
681
|
-
return {
|
|
682
|
-
repo_path: targetPath,
|
|
683
|
-
repo_name: repoName,
|
|
684
|
-
...parsed
|
|
685
|
-
};
|
|
686
|
-
}
|
|
687
|
-
var workspaceStateTool = tool3({
|
|
688
|
-
description: "Manage workspace state across multiple repos: read workspace context, update context, list sub-repos, get sub-repo state",
|
|
689
|
-
args: {
|
|
690
|
-
action: tool3.schema.enum(["read_context", "update_context", "list_repos", "get_repo_state"]),
|
|
691
|
-
updates: tool3.schema.object({
|
|
692
|
-
current_repo: tool3.schema.string().optional(),
|
|
693
|
-
status: tool3.schema.string().optional(),
|
|
694
|
-
phase: tool3.schema.number().optional()
|
|
695
|
-
}).optional(),
|
|
696
|
-
repo: tool3.schema.string().optional()
|
|
697
|
-
},
|
|
698
|
-
async execute(args, context) {
|
|
699
|
-
const dir = context.directory ?? process.cwd();
|
|
700
|
-
const workspaceRoot = findWorkspaceRoot(dir);
|
|
701
|
-
if (!workspaceRoot) {
|
|
702
|
-
return JSON.stringify({ error: "Workspace root not found. No config.json with sub_repos found." });
|
|
703
|
-
}
|
|
704
|
-
const config = getWorkspaceConfig(dir);
|
|
705
|
-
if (!config) {
|
|
706
|
-
return JSON.stringify({ error: "Could not read workspace config." });
|
|
707
|
-
}
|
|
708
|
-
const mode = config.workspace_mode;
|
|
709
|
-
const subRepos = config.sub_repos || [];
|
|
710
|
-
switch (args.action) {
|
|
711
|
-
case "read_context":
|
|
712
|
-
return JSON.stringify(await readWorkspaceContextAction(dir, mode, workspaceRoot));
|
|
713
|
-
case "update_context":
|
|
714
|
-
return JSON.stringify(await updateWorkspaceContextAction(dir, mode, workspaceRoot, args.updates));
|
|
715
|
-
case "list_repos":
|
|
716
|
-
return JSON.stringify(await listSubReposAction(dir, subRepos, workspaceRoot));
|
|
717
|
-
case "get_repo_state":
|
|
718
|
-
return JSON.stringify(await getSubRepoStateAction(dir, args.repo, subRepos, workspaceRoot, mode));
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
});
|
|
722
|
-
|
|
723
572
|
// src/tools/run-pipeline.ts
|
|
724
|
-
import { tool as
|
|
573
|
+
import { tool as tool3 } from "@opencode-ai/plugin";
|
|
725
574
|
|
|
726
575
|
// src/services/agent-performance.ts
|
|
727
|
-
import { existsSync as
|
|
728
|
-
import { join as
|
|
576
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync2 } from "fs";
|
|
577
|
+
import { join as join5 } from "path";
|
|
729
578
|
function perfPath(dir) {
|
|
730
|
-
return
|
|
579
|
+
return join5(codebaseDir(dir), "AGENT_PERF.json");
|
|
731
580
|
}
|
|
732
581
|
function loadStore(dir) {
|
|
733
582
|
const p = perfPath(dir);
|
|
734
|
-
if (!
|
|
583
|
+
if (!existsSync5(p))
|
|
735
584
|
return { entries: [], updated_at: new Date().toISOString() };
|
|
736
585
|
try {
|
|
737
|
-
return JSON.parse(
|
|
586
|
+
return JSON.parse(readFileSync5(p, "utf-8"));
|
|
738
587
|
} catch {
|
|
739
588
|
return { entries: [], updated_at: new Date().toISOString() };
|
|
740
589
|
}
|
|
741
590
|
}
|
|
742
591
|
function saveStore(dir, store) {
|
|
743
592
|
const cd = codebaseDir(dir);
|
|
744
|
-
if (!
|
|
593
|
+
if (!existsSync5(cd))
|
|
745
594
|
mkdirSync2(cd, { recursive: true });
|
|
746
|
-
|
|
595
|
+
writeFileSync4(perfPath(dir), JSON.stringify(store, null, 2), "utf-8");
|
|
747
596
|
}
|
|
748
597
|
function makeKey(agent, model, task_type) {
|
|
749
598
|
return `${agent}::${model}::${task_type}`;
|
|
@@ -867,227 +716,24 @@ function isUiHeavyTask(input) {
|
|
|
867
716
|
return !hasOnlyNonUiSignals;
|
|
868
717
|
}
|
|
869
718
|
|
|
870
|
-
// src/services/activity-reporter.ts
|
|
871
|
-
var SUMMARY_MAX_NORMAL = 120;
|
|
872
|
-
var SUMMARY_MAX_DEBUG = 600;
|
|
873
|
-
var HEARTBEAT_INTERVAL_MS = 15000;
|
|
874
|
-
var TOAST_ON_START_TOOLS = new Set(["delegate", "run-pipeline"]);
|
|
875
|
-
function isDebugMode() {
|
|
876
|
-
return process.env.FLOWDECK_DEBUG === "true" || process.env.FLOWDECK_DEBUG === "1";
|
|
877
|
-
}
|
|
878
|
-
function summarize(text, maxLen = SUMMARY_MAX_NORMAL) {
|
|
879
|
-
if (!text)
|
|
880
|
-
return "";
|
|
881
|
-
const s = text.trim().replace(/\s+/g, " ");
|
|
882
|
-
return s.length <= maxLen ? s : s.slice(0, maxLen - 1) + "…";
|
|
883
|
-
}
|
|
884
|
-
function fmtDuration(ms) {
|
|
885
|
-
if (ms < 1000)
|
|
886
|
-
return `${ms}ms`;
|
|
887
|
-
return `${(ms / 1000).toFixed(1)}s`;
|
|
888
|
-
}
|
|
889
|
-
function normalizeCommandName(raw) {
|
|
890
|
-
return raw.replace(/^\//, "").replace(/^fd-/, "");
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
class ActivityReporter {
|
|
894
|
-
log;
|
|
895
|
-
toastFn;
|
|
896
|
-
startTimes = new Map;
|
|
897
|
-
heartbeats = new Map;
|
|
898
|
-
constructor(log, toast) {
|
|
899
|
-
this.log = log;
|
|
900
|
-
this.toastFn = toast;
|
|
901
|
-
}
|
|
902
|
-
emit(msg) {
|
|
903
|
-
try {
|
|
904
|
-
this.log(msg);
|
|
905
|
-
} catch {}
|
|
906
|
-
}
|
|
907
|
-
toastNow(msg, variant, duration) {
|
|
908
|
-
if (!this.toastFn)
|
|
909
|
-
return;
|
|
910
|
-
try {
|
|
911
|
-
this.toastFn(msg, variant, duration);
|
|
912
|
-
} catch {}
|
|
913
|
-
}
|
|
914
|
-
trackStart(key) {
|
|
915
|
-
this.startTimes.set(key, Date.now());
|
|
916
|
-
const toolName = key.split(":").pop() ?? key;
|
|
917
|
-
const interval = setInterval(() => {
|
|
918
|
-
const startMs = this.startTimes.get(key);
|
|
919
|
-
if (startMs === undefined)
|
|
920
|
-
return;
|
|
921
|
-
const elapsed = Date.now() - startMs;
|
|
922
|
-
const msg = `[⋯ ${toolName}] still running (${fmtDuration(elapsed)})`;
|
|
923
|
-
this.emit(msg);
|
|
924
|
-
this.toastNow(msg, "info", 8000);
|
|
925
|
-
}, HEARTBEAT_INTERVAL_MS);
|
|
926
|
-
if (typeof interval.unref === "function") {
|
|
927
|
-
interval.unref();
|
|
928
|
-
}
|
|
929
|
-
this.heartbeats.set(key, interval);
|
|
930
|
-
}
|
|
931
|
-
elapsedMs(key) {
|
|
932
|
-
const interval = this.heartbeats.get(key);
|
|
933
|
-
if (interval !== undefined) {
|
|
934
|
-
clearInterval(interval);
|
|
935
|
-
this.heartbeats.delete(key);
|
|
936
|
-
}
|
|
937
|
-
const t = this.startTimes.get(key);
|
|
938
|
-
if (t === undefined)
|
|
939
|
-
return;
|
|
940
|
-
this.startTimes.delete(key);
|
|
941
|
-
return Date.now() - t;
|
|
942
|
-
}
|
|
943
|
-
reportToolStarted(tool4, inputSummary, meta = {}) {
|
|
944
|
-
const maxLen = isDebugMode() ? SUMMARY_MAX_DEBUG : SUMMARY_MAX_NORMAL;
|
|
945
|
-
const parts = [`[→ ${tool4}]`];
|
|
946
|
-
if (meta.agent)
|
|
947
|
-
parts.push(`agent=${meta.agent}`);
|
|
948
|
-
if (inputSummary)
|
|
949
|
-
parts.push(summarize(inputSummary, maxLen));
|
|
950
|
-
if (isDebugMode()) {
|
|
951
|
-
if (meta.session_id)
|
|
952
|
-
parts.push(`session=${meta.session_id}`);
|
|
953
|
-
if (meta.stage)
|
|
954
|
-
parts.push(`stage=${meta.stage}`);
|
|
955
|
-
if (meta.run_id)
|
|
956
|
-
parts.push(`run=${meta.run_id}`);
|
|
957
|
-
}
|
|
958
|
-
this.emit(parts.join(" "));
|
|
959
|
-
if (TOAST_ON_START_TOOLS.has(tool4)) {
|
|
960
|
-
const agentPart = meta.agent ? ` @${meta.agent}` : "";
|
|
961
|
-
const inputPart = inputSummary ? `: ${summarize(inputSummary, 60)}` : "";
|
|
962
|
-
this.toastNow(`→ ${tool4}${agentPart}${inputPart}`, "info", 3000);
|
|
963
|
-
}
|
|
964
|
-
}
|
|
965
|
-
reportToolCompleted(tool4, durationMs, resultSummary, meta = {}) {
|
|
966
|
-
const maxLen = isDebugMode() ? SUMMARY_MAX_DEBUG : SUMMARY_MAX_NORMAL;
|
|
967
|
-
const dur = durationMs !== undefined ? ` (${fmtDuration(durationMs)})` : "";
|
|
968
|
-
const parts = [`[✓ ${tool4}]${dur}`];
|
|
969
|
-
if (meta.agent)
|
|
970
|
-
parts.push(`agent=${meta.agent}`);
|
|
971
|
-
if (resultSummary)
|
|
972
|
-
parts.push(summarize(resultSummary, maxLen));
|
|
973
|
-
if (isDebugMode() && meta.retry_count && meta.retry_count > 0) {
|
|
974
|
-
parts.push(`retries=${meta.retry_count}`);
|
|
975
|
-
}
|
|
976
|
-
this.emit(parts.join(" "));
|
|
977
|
-
}
|
|
978
|
-
reportToolFailed(tool4, durationMs, error, meta = {}) {
|
|
979
|
-
const dur = durationMs !== undefined ? ` (${fmtDuration(durationMs)})` : "";
|
|
980
|
-
const parts = [`[✗ ${tool4}]${dur}`];
|
|
981
|
-
if (meta.agent)
|
|
982
|
-
parts.push(`agent=${meta.agent}`);
|
|
983
|
-
parts.push(`error=${summarize(error, isDebugMode() ? SUMMARY_MAX_DEBUG : 200)}`);
|
|
984
|
-
if (isDebugMode() && meta.retry_count && meta.retry_count > 0) {
|
|
985
|
-
parts.push(`retries=${meta.retry_count}`);
|
|
986
|
-
}
|
|
987
|
-
this.emit(parts.join(" "));
|
|
988
|
-
this.toastNow(`✗ ${tool4}${dur}: ${summarize(error, 80)}`, "error", 8000);
|
|
989
|
-
}
|
|
990
|
-
reportToolRetried(tool4, attempt, reason, meta = {}) {
|
|
991
|
-
const parts = [`[↺ ${tool4}] retry attempt=${attempt}`];
|
|
992
|
-
if (meta.agent)
|
|
993
|
-
parts.push(`agent=${meta.agent}`);
|
|
994
|
-
if (reason)
|
|
995
|
-
parts.push(`reason=${summarize(reason, 80)}`);
|
|
996
|
-
this.emit(parts.join(" "));
|
|
997
|
-
this.toastNow(`↺ ${tool4} retry #${attempt}${meta.agent ? ` @${meta.agent}` : ""}`, "warning", 5000);
|
|
998
|
-
}
|
|
999
|
-
reportToolFallback(fromTool, toTool, reason, meta = {}) {
|
|
1000
|
-
const parts = [`[⇢ fallback] ${fromTool} → ${toTool}`];
|
|
1001
|
-
if (reason)
|
|
1002
|
-
parts.push(`reason=${summarize(reason, 80)}`);
|
|
1003
|
-
if (meta.agent)
|
|
1004
|
-
parts.push(`agent=${meta.agent}`);
|
|
1005
|
-
this.emit(parts.join(" "));
|
|
1006
|
-
this.toastNow(`⇢ fallback: ${fromTool} → ${toTool}`, "info", 4000);
|
|
1007
|
-
}
|
|
1008
|
-
reportCacheHit(tool4, agent, meta = {}) {
|
|
1009
|
-
const parts = [`[≡ ${tool4}] cache hit agent=${agent}`];
|
|
1010
|
-
if (isDebugMode() && meta.session_id)
|
|
1011
|
-
parts.push(`session=${meta.session_id}`);
|
|
1012
|
-
this.emit(parts.join(" "));
|
|
1013
|
-
}
|
|
1014
|
-
reportSkipped(tool4, reason, meta = {}) {
|
|
1015
|
-
const parts = [`[⊘ ${tool4}] skipped`];
|
|
1016
|
-
if (reason)
|
|
1017
|
-
parts.push(`reason=${summarize(reason, 80)}`);
|
|
1018
|
-
if (meta.agent)
|
|
1019
|
-
parts.push(`agent=${meta.agent}`);
|
|
1020
|
-
this.emit(parts.join(" "));
|
|
1021
|
-
}
|
|
1022
|
-
reportStageProgress(stage, status, detail, meta = {}) {
|
|
1023
|
-
const icon = {
|
|
1024
|
-
started: "▶",
|
|
1025
|
-
running: "⋯",
|
|
1026
|
-
complete: "●",
|
|
1027
|
-
failed: "✗",
|
|
1028
|
-
waiting: "⌛"
|
|
1029
|
-
};
|
|
1030
|
-
const sym = icon[status] ?? "·";
|
|
1031
|
-
const parts = [`[${sym} ${stage}] ${status}`];
|
|
1032
|
-
if (detail)
|
|
1033
|
-
parts.push(summarize(detail));
|
|
1034
|
-
if (isDebugMode() && meta.workflow_id)
|
|
1035
|
-
parts.push(`workflow=${meta.workflow_id}`);
|
|
1036
|
-
this.emit(parts.join(" "));
|
|
1037
|
-
const detailPart = detail ? `: ${summarize(detail, 60)}` : "";
|
|
1038
|
-
switch (status) {
|
|
1039
|
-
case "started":
|
|
1040
|
-
this.toastNow(`▶ ${stage} started${detailPart}`, "info", 3000);
|
|
1041
|
-
break;
|
|
1042
|
-
case "complete":
|
|
1043
|
-
this.toastNow(`● ${stage} complete${detailPart}`, "success", 4000);
|
|
1044
|
-
break;
|
|
1045
|
-
case "failed":
|
|
1046
|
-
this.toastNow(`✗ ${stage} failed${detailPart}`, "error", 8000);
|
|
1047
|
-
break;
|
|
1048
|
-
case "waiting":
|
|
1049
|
-
this.toastNow(`⌛ ${stage}: waiting for input${detailPart}`, "warning", 30000);
|
|
1050
|
-
break;
|
|
1051
|
-
}
|
|
1052
|
-
}
|
|
1053
|
-
reportWaitingForApproval(tool4, _meta = {}) {
|
|
1054
|
-
const msg = `⌛ Approval required: ${tool4}`;
|
|
1055
|
-
this.emit(msg);
|
|
1056
|
-
this.toastNow(msg, "warning", 30000);
|
|
1057
|
-
}
|
|
1058
|
-
reportCommandStarted(command) {
|
|
1059
|
-
const cmd = normalizeCommandName(command);
|
|
1060
|
-
const msg = `▶ /${cmd} started`;
|
|
1061
|
-
this.emit(msg);
|
|
1062
|
-
this.toastNow(msg, "info", 2500);
|
|
1063
|
-
}
|
|
1064
|
-
reportCommandCompleted(command, hasEdits) {
|
|
1065
|
-
const cmd = normalizeCommandName(command);
|
|
1066
|
-
const detail = hasEdits ? " (files modified)" : "";
|
|
1067
|
-
const msg = `● /${cmd} complete${detail}`;
|
|
1068
|
-
this.emit(msg);
|
|
1069
|
-
this.toastNow(msg, "success", 5000);
|
|
1070
|
-
}
|
|
1071
|
-
}
|
|
1072
|
-
|
|
1073
719
|
// src/tools/run-pipeline.ts
|
|
1074
720
|
function extractText(parts) {
|
|
1075
721
|
return parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join(`
|
|
1076
722
|
`);
|
|
1077
723
|
}
|
|
1078
|
-
function createRunPipelineTool(client
|
|
1079
|
-
return
|
|
724
|
+
function createRunPipelineTool(client) {
|
|
725
|
+
return tool3({
|
|
1080
726
|
description: "Run agents in sequential pipeline. Each step's output is appended to the next step's context. One fresh child session per step. Returns full trace with session ID, input/output/duration per step.",
|
|
1081
727
|
args: {
|
|
1082
|
-
steps:
|
|
1083
|
-
agent:
|
|
1084
|
-
prompt:
|
|
1085
|
-
task_type:
|
|
728
|
+
steps: tool3.schema.array(tool3.schema.object({
|
|
729
|
+
agent: tool3.schema.string(),
|
|
730
|
+
prompt: tool3.schema.string(),
|
|
731
|
+
task_type: tool3.schema.string().optional()
|
|
1086
732
|
})),
|
|
1087
|
-
initial_context:
|
|
1088
|
-
abort_on_failure:
|
|
1089
|
-
retry_attempts:
|
|
1090
|
-
max_carry_chars:
|
|
733
|
+
initial_context: tool3.schema.string().optional(),
|
|
734
|
+
abort_on_failure: tool3.schema.boolean().optional().default(true),
|
|
735
|
+
retry_attempts: tool3.schema.number().optional().default(1),
|
|
736
|
+
max_carry_chars: tool3.schema.number().optional()
|
|
1091
737
|
},
|
|
1092
738
|
async execute(args, context) {
|
|
1093
739
|
const startTime = Date.now();
|
|
@@ -1097,7 +743,6 @@ function createRunPipelineTool(client, reporter) {
|
|
|
1097
743
|
const retryAttempts = typeof args.retry_attempts === "number" ? args.retry_attempts : 1;
|
|
1098
744
|
const maxRetries = Math.max(0, Math.floor(retryAttempts));
|
|
1099
745
|
const totalSteps = args.steps.length;
|
|
1100
|
-
reporter?.reportStageProgress("pipeline", "started", `${totalSteps} step(s)`);
|
|
1101
746
|
let inflightChildId = null;
|
|
1102
747
|
const abortHandler = () => {
|
|
1103
748
|
if (inflightChildId) {
|
|
@@ -1122,10 +767,6 @@ function createRunPipelineTool(client, reporter) {
|
|
|
1122
767
|
---
|
|
1123
768
|
|
|
1124
769
|
${step.prompt}` : step.prompt;
|
|
1125
|
-
reporter?.reportToolStarted("run-pipeline", summarize(step.prompt, 80), {
|
|
1126
|
-
agent: step.agent,
|
|
1127
|
-
stage: `step ${stepIdx + 1}/${totalSteps}`
|
|
1128
|
-
});
|
|
1129
770
|
const createRes = await client.session.create({
|
|
1130
771
|
body: { parentID: context.sessionID, title: `${step.agent}-pipeline` },
|
|
1131
772
|
query: { directory: context.directory }
|
|
@@ -1133,7 +774,6 @@ ${step.prompt}` : step.prompt;
|
|
|
1133
774
|
if (createRes.error || !createRes.data?.id) {
|
|
1134
775
|
const errMsg = `Failed to create session: ${createRes.error?.detail ?? "unknown"}`;
|
|
1135
776
|
trace.push({ agent: step.agent, task_type: taskType, model: "", input: stepInput, output: errMsg, duration_ms: Date.now() - stepStart, success: false });
|
|
1136
|
-
reporter?.reportToolFailed("run-pipeline", Date.now() - stepStart, errMsg, { agent: step.agent });
|
|
1137
777
|
aborted = true;
|
|
1138
778
|
break;
|
|
1139
779
|
}
|
|
@@ -1153,7 +793,6 @@ ${step.prompt}` : step.prompt;
|
|
|
1153
793
|
if (!shouldRetry(promptRes) || attempt === maxRetries)
|
|
1154
794
|
break;
|
|
1155
795
|
retriesUsed++;
|
|
1156
|
-
reporter?.reportToolRetried("run-pipeline", retriesUsed, "prompt response indicated retry", { agent: step.agent });
|
|
1157
796
|
}
|
|
1158
797
|
inflightChildId = null;
|
|
1159
798
|
if (context.abort.aborted) {
|
|
@@ -1164,7 +803,6 @@ ${step.prompt}` : step.prompt;
|
|
|
1164
803
|
const errMsg = `Prompt failed: ${promptRes?.error?.detail ?? "unknown"}`;
|
|
1165
804
|
trace.push({ agent: step.agent, session_id: createRes.data.id, task_type: taskType, model: "", input: stepInput, output: `${errMsg}${retriesUsed > 0 ? ` (retries: ${retriesUsed})` : ""}`, duration_ms: Date.now() - stepStart, success: false });
|
|
1166
805
|
recordRun(context.directory, step.agent, "", taskType, false, Date.now() - stepStart);
|
|
1167
|
-
reporter?.reportToolFailed("run-pipeline", Date.now() - stepStart, errMsg, { agent: step.agent, retry_count: retriesUsed });
|
|
1168
806
|
if (args.abort_on_failure) {
|
|
1169
807
|
aborted = true;
|
|
1170
808
|
break;
|
|
@@ -1176,7 +814,6 @@ ${step.prompt}` : step.prompt;
|
|
|
1176
814
|
const errMsg = `Agent error: ${JSON.stringify(info.error)}`;
|
|
1177
815
|
trace.push({ agent: step.agent, session_id: createRes.data.id, task_type: taskType, model: "", input: stepInput, output: `${errMsg}${retriesUsed > 0 ? ` (retries: ${retriesUsed})` : ""}`, duration_ms: Date.now() - stepStart, success: false });
|
|
1178
816
|
recordRun(context.directory, step.agent, "", taskType, false, Date.now() - stepStart);
|
|
1179
|
-
reporter?.reportToolFailed("run-pipeline", Date.now() - stepStart, errMsg, { agent: step.agent, retry_count: retriesUsed });
|
|
1180
817
|
if (args.abort_on_failure) {
|
|
1181
818
|
aborted = true;
|
|
1182
819
|
break;
|
|
@@ -1186,11 +823,6 @@ ${step.prompt}` : step.prompt;
|
|
|
1186
823
|
const output = extractText(promptRes.data?.parts ?? []);
|
|
1187
824
|
trace.push({ agent: step.agent, session_id: createRes.data.id, task_type: taskType, model: "", input: stepInput, output: output || "(no text output)", duration_ms: Date.now() - stepStart, success: true, context_chars: carryContext.length });
|
|
1188
825
|
recordRun(context.directory, step.agent, "", taskType, true, Date.now() - stepStart);
|
|
1189
|
-
reporter?.reportToolCompleted("run-pipeline", Date.now() - stepStart, summarize(output, 80), {
|
|
1190
|
-
agent: step.agent,
|
|
1191
|
-
retry_count: retriesUsed,
|
|
1192
|
-
stage: `step ${stepIdx + 1}/${totalSteps}`
|
|
1193
|
-
});
|
|
1194
826
|
const rawOutput = output || "";
|
|
1195
827
|
carryContext = typeof args.max_carry_chars === "number" && rawOutput.length > args.max_carry_chars ? rawOutput.slice(rawOutput.length - args.max_carry_chars) : rawOutput;
|
|
1196
828
|
}
|
|
@@ -1198,11 +830,6 @@ ${step.prompt}` : step.prompt;
|
|
|
1198
830
|
context.abort.removeEventListener("abort", abortHandler);
|
|
1199
831
|
}
|
|
1200
832
|
const totalDuration = Date.now() - startTime;
|
|
1201
|
-
if (aborted) {
|
|
1202
|
-
reporter?.reportStageProgress("pipeline", "failed", `aborted after ${trace.length}/${totalSteps} steps`);
|
|
1203
|
-
} else {
|
|
1204
|
-
reporter?.reportStageProgress("pipeline", "complete", `${totalSteps} step(s) in ${totalDuration}ms`);
|
|
1205
|
-
}
|
|
1206
833
|
return JSON.stringify({
|
|
1207
834
|
steps: trace,
|
|
1208
835
|
total_duration_ms: totalDuration,
|
|
@@ -1213,13 +840,13 @@ ${step.prompt}` : step.prompt;
|
|
|
1213
840
|
}
|
|
1214
841
|
|
|
1215
842
|
// src/tools/delegate.ts
|
|
1216
|
-
import { tool as
|
|
1217
|
-
import { existsSync as
|
|
843
|
+
import { tool as tool4 } from "@opencode-ai/plugin";
|
|
844
|
+
import { existsSync as existsSync10, readFileSync as readFileSync10 } from "fs";
|
|
1218
845
|
|
|
1219
846
|
// src/services/prompt-cache.ts
|
|
1220
847
|
import { createHash } from "crypto";
|
|
1221
|
-
import { existsSync as
|
|
1222
|
-
import { join as
|
|
848
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync5, readdirSync as readdirSync3, statSync, mkdirSync as mkdirSync3 } from "fs";
|
|
849
|
+
import { join as join6 } from "path";
|
|
1223
850
|
var CACHEABLE_AGENTS = new Set([
|
|
1224
851
|
"researcher",
|
|
1225
852
|
"code-explorer",
|
|
@@ -1233,17 +860,17 @@ var CACHE_DIR_NAME = "prompt-cache";
|
|
|
1233
860
|
var MAX_CACHE_ENTRIES = 200;
|
|
1234
861
|
var DEFAULT_TTL_MS = 30 * 60 * 1000;
|
|
1235
862
|
function cacheDir(dir) {
|
|
1236
|
-
return
|
|
863
|
+
return join6(codebaseDir(dir), CACHE_DIR_NAME);
|
|
1237
864
|
}
|
|
1238
865
|
function entryPath(dir, key) {
|
|
1239
|
-
return
|
|
866
|
+
return join6(cacheDir(dir), `${key}.json`);
|
|
1240
867
|
}
|
|
1241
868
|
function readEntry(dir, key, stateVersion, indexVersion) {
|
|
1242
869
|
const path = entryPath(dir, key);
|
|
1243
|
-
if (!
|
|
870
|
+
if (!existsSync6(path))
|
|
1244
871
|
return null;
|
|
1245
872
|
try {
|
|
1246
|
-
const entry = JSON.parse(
|
|
873
|
+
const entry = JSON.parse(readFileSync6(path, "utf-8"));
|
|
1247
874
|
const age = Date.now() - new Date(entry.created_at).getTime();
|
|
1248
875
|
if (age > entry.ttl_ms)
|
|
1249
876
|
return null;
|
|
@@ -1291,7 +918,7 @@ function setCached(dir, agent, prompt, context, stateVersion, indexVersion, resp
|
|
|
1291
918
|
if (!CACHEABLE_AGENTS.has(agent))
|
|
1292
919
|
return;
|
|
1293
920
|
const cd = cacheDir(dir);
|
|
1294
|
-
if (!
|
|
921
|
+
if (!existsSync6(cd))
|
|
1295
922
|
mkdirSync3(cd, { recursive: true });
|
|
1296
923
|
const key = hashKey(agent, prompt, context, stateVersion, indexVersion);
|
|
1297
924
|
const entry = {
|
|
@@ -1303,21 +930,21 @@ function setCached(dir, agent, prompt, context, stateVersion, indexVersion, resp
|
|
|
1303
930
|
ttl_ms,
|
|
1304
931
|
response
|
|
1305
932
|
};
|
|
1306
|
-
|
|
933
|
+
writeFileSync5(entryPath(dir, key), JSON.stringify(entry, null, 2), "utf-8");
|
|
1307
934
|
pruneExpired(dir);
|
|
1308
935
|
}
|
|
1309
936
|
function pruneExpired(dir) {
|
|
1310
937
|
const cd = cacheDir(dir);
|
|
1311
|
-
if (!
|
|
938
|
+
if (!existsSync6(cd))
|
|
1312
939
|
return;
|
|
1313
940
|
try {
|
|
1314
941
|
const files = readdirSync3(cd).filter((f) => f.endsWith(".json"));
|
|
1315
942
|
const now = Date.now();
|
|
1316
943
|
const entries = [];
|
|
1317
944
|
for (const f of files) {
|
|
1318
|
-
const p =
|
|
945
|
+
const p = join6(cd, f);
|
|
1319
946
|
try {
|
|
1320
|
-
const entry = JSON.parse(
|
|
947
|
+
const entry = JSON.parse(readFileSync6(p, "utf-8"));
|
|
1321
948
|
const age = now - new Date(entry.created_at).getTime();
|
|
1322
949
|
entries.push({ path: p, created_at: new Date(entry.created_at).getTime(), expired: age > entry.ttl_ms });
|
|
1323
950
|
} catch {
|
|
@@ -1344,15 +971,15 @@ function pruneExpired(dir) {
|
|
|
1344
971
|
}
|
|
1345
972
|
|
|
1346
973
|
// src/tools/codebase-index.ts
|
|
1347
|
-
import { readFileSync as
|
|
1348
|
-
import { join as
|
|
974
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync6, existsSync as existsSync7, mkdirSync as mkdirSync4 } from "fs";
|
|
975
|
+
import { join as join7 } from "path";
|
|
1349
976
|
var CODEBASE_INDEX_FILE = "CODEBASE_INDEX.md";
|
|
1350
977
|
function indexPath(dir) {
|
|
1351
|
-
return
|
|
978
|
+
return join7(planningDir(dir), CODEBASE_INDEX_FILE);
|
|
1352
979
|
}
|
|
1353
980
|
function readCodebaseIndex(dir) {
|
|
1354
981
|
const path = indexPath(dir);
|
|
1355
|
-
if (!
|
|
982
|
+
if (!existsSync7(path)) {
|
|
1356
983
|
return {
|
|
1357
984
|
exists: false,
|
|
1358
985
|
lastUpdatedAt: "",
|
|
@@ -1366,7 +993,7 @@ function readCodebaseIndex(dir) {
|
|
|
1366
993
|
};
|
|
1367
994
|
}
|
|
1368
995
|
try {
|
|
1369
|
-
const content =
|
|
996
|
+
const content = readFileSync7(path, "utf-8");
|
|
1370
997
|
return parseCodebaseIndexContent(content);
|
|
1371
998
|
} catch {
|
|
1372
999
|
return {
|
|
@@ -1442,17 +1069,17 @@ function parseCodebaseIndexContent(content) {
|
|
|
1442
1069
|
}
|
|
1443
1070
|
|
|
1444
1071
|
// src/services/token-metrics.ts
|
|
1445
|
-
import { existsSync as
|
|
1446
|
-
import { join as
|
|
1072
|
+
import { existsSync as existsSync8, readFileSync as readFileSync8, appendFileSync, mkdirSync as mkdirSync5 } from "fs";
|
|
1073
|
+
import { join as join8 } from "path";
|
|
1447
1074
|
function estimateTokens(text) {
|
|
1448
1075
|
return Math.ceil(text.length / 4);
|
|
1449
1076
|
}
|
|
1450
1077
|
function metricsPath(dir) {
|
|
1451
|
-
return
|
|
1078
|
+
return join8(codebaseDir(dir), "TOKEN_METRICS.jsonl");
|
|
1452
1079
|
}
|
|
1453
1080
|
function appendEvent(dir, event) {
|
|
1454
1081
|
const cd = codebaseDir(dir);
|
|
1455
|
-
if (!
|
|
1082
|
+
if (!existsSync8(cd))
|
|
1456
1083
|
mkdirSync5(cd, { recursive: true });
|
|
1457
1084
|
appendFileSync(metricsPath(dir), JSON.stringify(event) + `
|
|
1458
1085
|
`, "utf-8");
|
|
@@ -1510,27 +1137,25 @@ function recordRetryCall(dir, workflow_id, stage, inputText, outputText, agent,
|
|
|
1510
1137
|
var _workflowTimers = new Map;
|
|
1511
1138
|
|
|
1512
1139
|
// src/config/loader.ts
|
|
1513
|
-
import { existsSync as
|
|
1514
|
-
import { join as
|
|
1140
|
+
import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
|
|
1141
|
+
import { join as join9 } from "path";
|
|
1515
1142
|
import { homedir } from "os";
|
|
1516
1143
|
var CONFIG_FILENAME = "flowdeck.json";
|
|
1517
1144
|
function getGlobalConfigDir() {
|
|
1518
|
-
return process.env.OPENCODE_CONFIG_DIR || (process.env.XDG_CONFIG_HOME ?
|
|
1145
|
+
return process.env.OPENCODE_CONFIG_DIR || (process.env.XDG_CONFIG_HOME ? join9(process.env.XDG_CONFIG_HOME, "opencode") : join9(homedir(), ".config", "opencode"));
|
|
1519
1146
|
}
|
|
1520
1147
|
function loadFlowDeckConfig(directory) {
|
|
1521
1148
|
const candidates = [];
|
|
1522
1149
|
if (directory) {
|
|
1523
|
-
candidates.push(
|
|
1150
|
+
candidates.push(join9(directory, ".opencode", CONFIG_FILENAME));
|
|
1524
1151
|
}
|
|
1525
|
-
candidates.push(
|
|
1152
|
+
candidates.push(join9(getGlobalConfigDir(), CONFIG_FILENAME));
|
|
1526
1153
|
for (const configPath of candidates) {
|
|
1527
|
-
if (
|
|
1154
|
+
if (existsSync9(configPath)) {
|
|
1528
1155
|
try {
|
|
1529
|
-
const content =
|
|
1156
|
+
const content = readFileSync9(configPath, "utf-8");
|
|
1530
1157
|
return JSON.parse(content);
|
|
1531
|
-
} catch {
|
|
1532
|
-
console.warn(`[flowdeck] Failed to load config from ${configPath}`);
|
|
1533
|
-
}
|
|
1158
|
+
} catch {}
|
|
1534
1159
|
}
|
|
1535
1160
|
}
|
|
1536
1161
|
return {};
|
|
@@ -1611,19 +1236,19 @@ function extractText2(parts) {
|
|
|
1611
1236
|
return parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join(`
|
|
1612
1237
|
`);
|
|
1613
1238
|
}
|
|
1614
|
-
function createDelegateTool(client
|
|
1615
|
-
return
|
|
1239
|
+
function createDelegateTool(client) {
|
|
1240
|
+
return tool4({
|
|
1616
1241
|
description: "Delegate a task to a single agent via a child session. Returns the agent's output.",
|
|
1617
1242
|
args: {
|
|
1618
|
-
agent:
|
|
1619
|
-
prompt:
|
|
1620
|
-
context:
|
|
1621
|
-
task_type:
|
|
1622
|
-
retry_attempts:
|
|
1623
|
-
safe_to_cache:
|
|
1624
|
-
cache_ttl_ms:
|
|
1625
|
-
workflow_id:
|
|
1626
|
-
stage:
|
|
1243
|
+
agent: tool4.schema.string(),
|
|
1244
|
+
prompt: tool4.schema.string(),
|
|
1245
|
+
context: tool4.schema.string().optional(),
|
|
1246
|
+
task_type: tool4.schema.string().optional(),
|
|
1247
|
+
retry_attempts: tool4.schema.number().optional().default(1),
|
|
1248
|
+
safe_to_cache: tool4.schema.boolean().optional().default(false),
|
|
1249
|
+
cache_ttl_ms: tool4.schema.number().optional(),
|
|
1250
|
+
workflow_id: tool4.schema.string().optional(),
|
|
1251
|
+
stage: tool4.schema.string().optional()
|
|
1627
1252
|
},
|
|
1628
1253
|
async execute(args, context) {
|
|
1629
1254
|
const startTime = Date.now();
|
|
@@ -1642,17 +1267,13 @@ function createDelegateTool(client, reporter) {
|
|
|
1642
1267
|
---
|
|
1643
1268
|
|
|
1644
1269
|
${args.prompt}` : args.prompt;
|
|
1645
|
-
reporter?.reportToolStarted("delegate", summarize(args.prompt, 100), {
|
|
1646
|
-
agent: args.agent,
|
|
1647
|
-
stage: args.stage
|
|
1648
|
-
});
|
|
1649
1270
|
const safe_to_cache = args.safe_to_cache === true && CACHEABLE_AGENTS.has(args.agent);
|
|
1650
1271
|
let stateVersion = 0;
|
|
1651
1272
|
let indexVersion = 0;
|
|
1652
1273
|
if (safe_to_cache) {
|
|
1653
1274
|
const index = readCodebaseIndex(context.directory);
|
|
1654
1275
|
const sp = statePath(context.directory);
|
|
1655
|
-
const rawState =
|
|
1276
|
+
const rawState = existsSync10(sp) ? readFileSync10(sp, "utf-8") : "";
|
|
1656
1277
|
const state = rawState ? parseState(rawState) : {};
|
|
1657
1278
|
stateVersion = typeof state.summaryVersion === "number" ? state.summaryVersion : 0;
|
|
1658
1279
|
indexVersion = typeof index.summaryVersion === "number" ? index.summaryVersion : 0;
|
|
@@ -1661,7 +1282,6 @@ ${args.prompt}` : args.prompt;
|
|
|
1661
1282
|
if (metricsWorkflowId) {
|
|
1662
1283
|
recordCacheHit(context.directory, metricsWorkflowId, metricsStage, fullPrompt, args.agent, agentModel);
|
|
1663
1284
|
}
|
|
1664
|
-
reporter?.reportCacheHit("delegate", args.agent);
|
|
1665
1285
|
return JSON.stringify({
|
|
1666
1286
|
agent: args.agent,
|
|
1667
1287
|
success: true,
|
|
@@ -1719,15 +1339,10 @@ ${args.prompt}` : args.prompt;
|
|
|
1719
1339
|
recordRetryCall(context.directory, metricsWorkflowId, metricsStage, fullPromptForSession, "", args.agent, Date.now() - attemptStart, agentModel, retryCostUsd);
|
|
1720
1340
|
}
|
|
1721
1341
|
retriesUsed++;
|
|
1722
|
-
reporter?.reportToolRetried("delegate", retriesUsed, "prompt response indicated retry", { agent: args.agent });
|
|
1723
1342
|
}
|
|
1724
1343
|
if (!promptRes || promptRes.error) {
|
|
1725
1344
|
const errMsg = `Prompt failed: ${promptRes?.error?.detail ?? "unknown"}`;
|
|
1726
1345
|
recordRun(context.directory, args.agent, "", taskType, false, Date.now() - startTime);
|
|
1727
|
-
reporter?.reportToolFailed("delegate", Date.now() - startTime, errMsg, {
|
|
1728
|
-
agent: args.agent,
|
|
1729
|
-
retry_count: retriesUsed
|
|
1730
|
-
});
|
|
1731
1346
|
return JSON.stringify({
|
|
1732
1347
|
agent: args.agent,
|
|
1733
1348
|
session_id: childId,
|
|
@@ -1743,10 +1358,6 @@ ${args.prompt}` : args.prompt;
|
|
|
1743
1358
|
if (info?.error) {
|
|
1744
1359
|
const errMsg = `Agent error: ${JSON.stringify(info.error)}`;
|
|
1745
1360
|
recordRun(context.directory, args.agent, "", taskType, false, Date.now() - startTime);
|
|
1746
|
-
reporter?.reportToolFailed("delegate", Date.now() - startTime, errMsg, {
|
|
1747
|
-
agent: args.agent,
|
|
1748
|
-
retry_count: retriesUsed
|
|
1749
|
-
});
|
|
1750
1361
|
return JSON.stringify({
|
|
1751
1362
|
agent: args.agent,
|
|
1752
1363
|
session_id: childId,
|
|
@@ -1766,10 +1377,6 @@ ${args.prompt}` : args.prompt;
|
|
|
1766
1377
|
const costUsd = agentModel ? estimateCostUSD(agentModel, inputTokens, outputTokens) : undefined;
|
|
1767
1378
|
recordModelCall(context.directory, metricsWorkflowId, metricsStage, fullPromptForSession, output, args.agent, Date.now() - startTime, agentModel, costUsd);
|
|
1768
1379
|
}
|
|
1769
|
-
reporter?.reportToolCompleted("delegate", Date.now() - startTime, summarize(output, 80), {
|
|
1770
|
-
agent: args.agent,
|
|
1771
|
-
retry_count: retriesUsed
|
|
1772
|
-
});
|
|
1773
1380
|
if (safe_to_cache && output) {
|
|
1774
1381
|
setCached(context.directory, args.agent, fullPromptForSession, args.context ?? "", stateVersion, indexVersion, output, true, args.cache_ttl_ms);
|
|
1775
1382
|
}
|
|
@@ -1788,53 +1395,53 @@ ${args.prompt}` : args.prompt;
|
|
|
1788
1395
|
}
|
|
1789
1396
|
|
|
1790
1397
|
// src/tools/repo-memory.ts
|
|
1791
|
-
import { tool as
|
|
1792
|
-
import { readFileSync as
|
|
1793
|
-
import { join as
|
|
1398
|
+
import { tool as tool5 } from "@opencode-ai/plugin";
|
|
1399
|
+
import { readFileSync as readFileSync11, writeFileSync as writeFileSync7, existsSync as existsSync11, mkdirSync as mkdirSync6 } from "fs";
|
|
1400
|
+
import { join as join10 } from "path";
|
|
1794
1401
|
var MEMORY_FILE = "MEMORY.json";
|
|
1795
1402
|
function memoryPath(directory) {
|
|
1796
|
-
return
|
|
1403
|
+
return join10(codebaseDir(directory), MEMORY_FILE);
|
|
1797
1404
|
}
|
|
1798
1405
|
function emptyMemory() {
|
|
1799
1406
|
return { version: "1.0", last_updated: new Date().toISOString(), nodes: {} };
|
|
1800
1407
|
}
|
|
1801
1408
|
function readMemory(directory) {
|
|
1802
1409
|
const p = memoryPath(directory);
|
|
1803
|
-
if (!
|
|
1410
|
+
if (!existsSync11(p))
|
|
1804
1411
|
return emptyMemory();
|
|
1805
1412
|
try {
|
|
1806
|
-
return JSON.parse(
|
|
1413
|
+
return JSON.parse(readFileSync11(p, "utf-8"));
|
|
1807
1414
|
} catch {
|
|
1808
1415
|
return emptyMemory();
|
|
1809
1416
|
}
|
|
1810
1417
|
}
|
|
1811
1418
|
function writeMemory(directory, memory) {
|
|
1812
1419
|
const base = codebaseDir(directory);
|
|
1813
|
-
if (!
|
|
1420
|
+
if (!existsSync11(base))
|
|
1814
1421
|
mkdirSync6(base, { recursive: true });
|
|
1815
1422
|
memory.last_updated = new Date().toISOString();
|
|
1816
|
-
|
|
1423
|
+
writeFileSync7(memoryPath(directory), JSON.stringify(memory, null, 2), "utf-8");
|
|
1817
1424
|
}
|
|
1818
|
-
var repoMemoryTool =
|
|
1425
|
+
var repoMemoryTool = tool5({
|
|
1819
1426
|
description: "Repo Memory Graph: read/write/query persistent architecture graph in .codebase/MEMORY.json (modules, dependencies, ownership, bug history, conventions)",
|
|
1820
1427
|
args: {
|
|
1821
|
-
action:
|
|
1822
|
-
node_id:
|
|
1823
|
-
node:
|
|
1824
|
-
type:
|
|
1825
|
-
path:
|
|
1826
|
-
owner:
|
|
1827
|
-
tags:
|
|
1828
|
-
dependencies:
|
|
1829
|
-
dependents:
|
|
1830
|
-
bug_history:
|
|
1831
|
-
conventions:
|
|
1428
|
+
action: tool5.schema.enum(["read", "write_node", "query", "delete_node"]),
|
|
1429
|
+
node_id: tool5.schema.string().optional(),
|
|
1430
|
+
node: tool5.schema.object({
|
|
1431
|
+
type: tool5.schema.enum(["module", "service", "api", "schema", "config"]),
|
|
1432
|
+
path: tool5.schema.string(),
|
|
1433
|
+
owner: tool5.schema.string().optional(),
|
|
1434
|
+
tags: tool5.schema.array(tool5.schema.string()),
|
|
1435
|
+
dependencies: tool5.schema.array(tool5.schema.string()),
|
|
1436
|
+
dependents: tool5.schema.array(tool5.schema.string()),
|
|
1437
|
+
bug_history: tool5.schema.array(tool5.schema.string()),
|
|
1438
|
+
conventions: tool5.schema.array(tool5.schema.string())
|
|
1832
1439
|
}).optional(),
|
|
1833
|
-
query:
|
|
1834
|
-
type:
|
|
1835
|
-
owner:
|
|
1836
|
-
tag:
|
|
1837
|
-
path_prefix:
|
|
1440
|
+
query: tool5.schema.object({
|
|
1441
|
+
type: tool5.schema.enum(["module", "service", "api", "schema", "config"]).optional(),
|
|
1442
|
+
owner: tool5.schema.string().optional(),
|
|
1443
|
+
tag: tool5.schema.string().optional(),
|
|
1444
|
+
path_prefix: tool5.schema.string().optional()
|
|
1838
1445
|
}).optional()
|
|
1839
1446
|
},
|
|
1840
1447
|
async execute(args, context) {
|
|
@@ -1889,50 +1496,50 @@ var repoMemoryTool = tool6({
|
|
|
1889
1496
|
});
|
|
1890
1497
|
|
|
1891
1498
|
// src/tools/failure-replay.ts
|
|
1892
|
-
import { tool as
|
|
1893
|
-
import { readFileSync as
|
|
1894
|
-
import { join as
|
|
1499
|
+
import { tool as tool6 } from "@opencode-ai/plugin";
|
|
1500
|
+
import { readFileSync as readFileSync12, writeFileSync as writeFileSync8, existsSync as existsSync12, mkdirSync as mkdirSync7 } from "fs";
|
|
1501
|
+
import { join as join11 } from "path";
|
|
1895
1502
|
var FAILURES_FILE = "FAILURES.json";
|
|
1896
1503
|
function failuresPath(directory) {
|
|
1897
|
-
return
|
|
1504
|
+
return join11(codebaseDir(directory), FAILURES_FILE);
|
|
1898
1505
|
}
|
|
1899
1506
|
function readStore(directory) {
|
|
1900
1507
|
const p = failuresPath(directory);
|
|
1901
|
-
if (!
|
|
1508
|
+
if (!existsSync12(p))
|
|
1902
1509
|
return { version: "1.0", last_updated: new Date().toISOString(), entries: [] };
|
|
1903
1510
|
try {
|
|
1904
|
-
return JSON.parse(
|
|
1511
|
+
return JSON.parse(readFileSync12(p, "utf-8"));
|
|
1905
1512
|
} catch {
|
|
1906
1513
|
return { version: "1.0", last_updated: new Date().toISOString(), entries: [] };
|
|
1907
1514
|
}
|
|
1908
1515
|
}
|
|
1909
1516
|
function writeStore(directory, store) {
|
|
1910
1517
|
const base = codebaseDir(directory);
|
|
1911
|
-
if (!
|
|
1518
|
+
if (!existsSync12(base))
|
|
1912
1519
|
mkdirSync7(base, { recursive: true });
|
|
1913
1520
|
store.last_updated = new Date().toISOString();
|
|
1914
|
-
|
|
1521
|
+
writeFileSync8(failuresPath(directory), JSON.stringify(store, null, 2), "utf-8");
|
|
1915
1522
|
}
|
|
1916
|
-
var failureReplayTool =
|
|
1523
|
+
var failureReplayTool = tool6({
|
|
1917
1524
|
description: "Failure Replay Engine: record and query past failures (reverted commits, failed deployments, flaky tests, bug fixes) in .codebase/FAILURES.json so the agent avoids repeating mistakes",
|
|
1918
1525
|
args: {
|
|
1919
|
-
action:
|
|
1920
|
-
entry:
|
|
1921
|
-
id:
|
|
1922
|
-
type:
|
|
1923
|
-
description:
|
|
1924
|
-
affected_paths:
|
|
1925
|
-
root_cause:
|
|
1926
|
-
fix_applied:
|
|
1927
|
-
tags:
|
|
1526
|
+
action: tool6.schema.enum(["record", "query", "list", "mark_resolved"]),
|
|
1527
|
+
entry: tool6.schema.object({
|
|
1528
|
+
id: tool6.schema.string(),
|
|
1529
|
+
type: tool6.schema.enum(["reverted_commit", "failed_deployment", "flaky_test", "bug_fix", "build_failure"]),
|
|
1530
|
+
description: tool6.schema.string(),
|
|
1531
|
+
affected_paths: tool6.schema.array(tool6.schema.string()),
|
|
1532
|
+
root_cause: tool6.schema.string().optional(),
|
|
1533
|
+
fix_applied: tool6.schema.string().optional(),
|
|
1534
|
+
tags: tool6.schema.array(tool6.schema.string())
|
|
1928
1535
|
}).optional(),
|
|
1929
|
-
query:
|
|
1930
|
-
type:
|
|
1931
|
-
path_prefix:
|
|
1932
|
-
tag:
|
|
1933
|
-
limit:
|
|
1536
|
+
query: tool6.schema.object({
|
|
1537
|
+
type: tool6.schema.enum(["reverted_commit", "failed_deployment", "flaky_test", "bug_fix", "build_failure"]).optional(),
|
|
1538
|
+
path_prefix: tool6.schema.string().optional(),
|
|
1539
|
+
tag: tool6.schema.string().optional(),
|
|
1540
|
+
limit: tool6.schema.number().optional()
|
|
1934
1541
|
}).optional(),
|
|
1935
|
-
entry_id:
|
|
1542
|
+
entry_id: tool6.schema.string().optional()
|
|
1936
1543
|
},
|
|
1937
1544
|
async execute(args, context) {
|
|
1938
1545
|
const dir = context.directory ?? process.cwd();
|
|
@@ -1994,18 +1601,18 @@ var failureReplayTool = tool7({
|
|
|
1994
1601
|
});
|
|
1995
1602
|
|
|
1996
1603
|
// src/tools/decision-trace.ts
|
|
1997
|
-
import { tool as
|
|
1998
|
-
import { readFileSync as
|
|
1999
|
-
import { join as
|
|
1604
|
+
import { tool as tool7 } from "@opencode-ai/plugin";
|
|
1605
|
+
import { readFileSync as readFileSync13, existsSync as existsSync13, mkdirSync as mkdirSync8, appendFileSync as appendFileSync2 } from "fs";
|
|
1606
|
+
import { join as join12 } from "path";
|
|
2000
1607
|
var DECISIONS_FILE = "DECISIONS.jsonl";
|
|
2001
1608
|
function decisionsPath(directory) {
|
|
2002
|
-
return
|
|
1609
|
+
return join12(codebaseDir(directory), DECISIONS_FILE);
|
|
2003
1610
|
}
|
|
2004
1611
|
function readDecisions(directory) {
|
|
2005
1612
|
const p = decisionsPath(directory);
|
|
2006
|
-
if (!
|
|
1613
|
+
if (!existsSync13(p))
|
|
2007
1614
|
return [];
|
|
2008
|
-
return
|
|
1615
|
+
return readFileSync13(p, "utf-8").split(`
|
|
2009
1616
|
`).filter((l) => l.trim()).map((l) => {
|
|
2010
1617
|
try {
|
|
2011
1618
|
return JSON.parse(l);
|
|
@@ -2014,29 +1621,29 @@ function readDecisions(directory) {
|
|
|
2014
1621
|
}
|
|
2015
1622
|
}).filter(Boolean);
|
|
2016
1623
|
}
|
|
2017
|
-
var decisionTraceTool =
|
|
1624
|
+
var decisionTraceTool = tool7({
|
|
2018
1625
|
description: "Decision Trace: record why the agent changed something, what evidence was used, and assumptions made. Stored in .codebase/DECISIONS.jsonl for fast review.",
|
|
2019
1626
|
args: {
|
|
2020
|
-
action:
|
|
2021
|
-
entry:
|
|
2022
|
-
id:
|
|
2023
|
-
file_path:
|
|
2024
|
-
change_type:
|
|
2025
|
-
rationale:
|
|
2026
|
-
evidence:
|
|
2027
|
-
assumptions:
|
|
2028
|
-
alternatives_considered:
|
|
2029
|
-
risk_level:
|
|
2030
|
-
agent:
|
|
2031
|
-
session_id:
|
|
1627
|
+
action: tool7.schema.enum(["record", "query", "get_for_file"]),
|
|
1628
|
+
entry: tool7.schema.object({
|
|
1629
|
+
id: tool7.schema.string(),
|
|
1630
|
+
file_path: tool7.schema.string(),
|
|
1631
|
+
change_type: tool7.schema.enum(["create", "edit", "delete", "refactor"]),
|
|
1632
|
+
rationale: tool7.schema.string(),
|
|
1633
|
+
evidence: tool7.schema.array(tool7.schema.string()),
|
|
1634
|
+
assumptions: tool7.schema.array(tool7.schema.string()),
|
|
1635
|
+
alternatives_considered: tool7.schema.array(tool7.schema.string()),
|
|
1636
|
+
risk_level: tool7.schema.enum(["low", "medium", "high"]),
|
|
1637
|
+
agent: tool7.schema.string().optional(),
|
|
1638
|
+
session_id: tool7.schema.string().optional()
|
|
2032
1639
|
}).optional(),
|
|
2033
|
-
query:
|
|
2034
|
-
file_path:
|
|
2035
|
-
change_type:
|
|
2036
|
-
risk_level:
|
|
2037
|
-
limit:
|
|
1640
|
+
query: tool7.schema.object({
|
|
1641
|
+
file_path: tool7.schema.string().optional(),
|
|
1642
|
+
change_type: tool7.schema.enum(["create", "edit", "delete", "refactor"]).optional(),
|
|
1643
|
+
risk_level: tool7.schema.enum(["low", "medium", "high"]).optional(),
|
|
1644
|
+
limit: tool7.schema.number().optional()
|
|
2038
1645
|
}).optional(),
|
|
2039
|
-
file_path:
|
|
1646
|
+
file_path: tool7.schema.string().optional()
|
|
2040
1647
|
},
|
|
2041
1648
|
async execute(args, context) {
|
|
2042
1649
|
const dir = context.directory ?? process.cwd();
|
|
@@ -2045,7 +1652,7 @@ var decisionTraceTool = tool8({
|
|
|
2045
1652
|
case "record": {
|
|
2046
1653
|
if (!args.entry)
|
|
2047
1654
|
return JSON.stringify({ error: "entry required" });
|
|
2048
|
-
if (!
|
|
1655
|
+
if (!existsSync13(base))
|
|
2049
1656
|
mkdirSync8(base, { recursive: true });
|
|
2050
1657
|
const entry = { ...args.entry, timestamp: new Date().toISOString() };
|
|
2051
1658
|
appendFileSync2(decisionsPath(dir), JSON.stringify(entry) + `
|
|
@@ -2078,162 +1685,54 @@ var decisionTraceTool = tool8({
|
|
|
2078
1685
|
}
|
|
2079
1686
|
});
|
|
2080
1687
|
|
|
2081
|
-
// src/tools/volatility-map.ts
|
|
2082
|
-
import { tool as tool9 } from "@opencode-ai/plugin";
|
|
2083
|
-
import { readFileSync as readFileSync15, writeFileSync as writeFileSync11, existsSync as existsSync15, mkdirSync as mkdirSync9 } from "fs";
|
|
2084
|
-
import { join as join14 } from "path";
|
|
2085
|
-
var VOLATILITY_FILE = "VOLATILITY.json";
|
|
2086
|
-
function volatilityPath(directory) {
|
|
2087
|
-
return join14(codebaseDir(directory), VOLATILITY_FILE);
|
|
2088
|
-
}
|
|
2089
|
-
function readStore2(directory) {
|
|
2090
|
-
const p = volatilityPath(directory);
|
|
2091
|
-
if (!existsSync15(p))
|
|
2092
|
-
return { version: "1.0", last_updated: new Date().toISOString(), generated_at: new Date().toISOString(), entries: [] };
|
|
2093
|
-
try {
|
|
2094
|
-
return JSON.parse(readFileSync15(p, "utf-8"));
|
|
2095
|
-
} catch {
|
|
2096
|
-
return { version: "1.0", last_updated: new Date().toISOString(), generated_at: new Date().toISOString(), entries: [] };
|
|
2097
|
-
}
|
|
2098
|
-
}
|
|
2099
|
-
function writeStore2(directory, store) {
|
|
2100
|
-
const base = codebaseDir(directory);
|
|
2101
|
-
if (!existsSync15(base))
|
|
2102
|
-
mkdirSync9(base, { recursive: true });
|
|
2103
|
-
store.last_updated = new Date().toISOString();
|
|
2104
|
-
writeFileSync11(volatilityPath(directory), JSON.stringify(store, null, 2), "utf-8");
|
|
2105
|
-
}
|
|
2106
|
-
function stabilityLabel(churn, hotfixes, todos) {
|
|
2107
|
-
const score = churn + hotfixes * 10 + todos * 2;
|
|
2108
|
-
if (score >= 80)
|
|
2109
|
-
return "critical";
|
|
2110
|
-
if (score >= 50)
|
|
2111
|
-
return "volatile";
|
|
2112
|
-
if (score >= 20)
|
|
2113
|
-
return "moderate";
|
|
2114
|
-
return "stable";
|
|
2115
|
-
}
|
|
2116
|
-
var volatilityMapTool = tool9({
|
|
2117
|
-
description: "Codebase Volatility Map: read/write/query .codebase/VOLATILITY.json — highlights unstable zones based on churn, hotfix frequency, and TODO clusters",
|
|
2118
|
-
args: {
|
|
2119
|
-
action: tool9.schema.enum(["read", "write", "query_hotspots", "update_entry"]),
|
|
2120
|
-
entries: tool9.schema.array(tool9.schema.object({
|
|
2121
|
-
path: tool9.schema.string(),
|
|
2122
|
-
churn_score: tool9.schema.number(),
|
|
2123
|
-
hotfix_count: tool9.schema.number(),
|
|
2124
|
-
todo_count: tool9.schema.number(),
|
|
2125
|
-
last_breakage: tool9.schema.string().optional(),
|
|
2126
|
-
notes: tool9.schema.array(tool9.schema.string())
|
|
2127
|
-
})).optional(),
|
|
2128
|
-
entry: tool9.schema.object({
|
|
2129
|
-
path: tool9.schema.string(),
|
|
2130
|
-
churn_score: tool9.schema.number(),
|
|
2131
|
-
hotfix_count: tool9.schema.number(),
|
|
2132
|
-
todo_count: tool9.schema.number(),
|
|
2133
|
-
last_breakage: tool9.schema.string().optional(),
|
|
2134
|
-
notes: tool9.schema.array(tool9.schema.string())
|
|
2135
|
-
}).optional(),
|
|
2136
|
-
threshold: tool9.schema.enum(["stable", "moderate", "volatile", "critical"]).optional(),
|
|
2137
|
-
path_prefix: tool9.schema.string().optional(),
|
|
2138
|
-
limit: tool9.schema.number().optional()
|
|
2139
|
-
},
|
|
2140
|
-
async execute(args, context) {
|
|
2141
|
-
const dir = context.directory ?? process.cwd();
|
|
2142
|
-
const store = readStore2(dir);
|
|
2143
|
-
switch (args.action) {
|
|
2144
|
-
case "read": {
|
|
2145
|
-
return JSON.stringify({ last_updated: store.last_updated, count: store.entries.length, entries: store.entries });
|
|
2146
|
-
}
|
|
2147
|
-
case "write": {
|
|
2148
|
-
if (!args.entries)
|
|
2149
|
-
return JSON.stringify({ error: "entries required" });
|
|
2150
|
-
store.entries = args.entries.map((e) => ({
|
|
2151
|
-
...e,
|
|
2152
|
-
stability: stabilityLabel(e.churn_score, e.hotfix_count, e.todo_count)
|
|
2153
|
-
}));
|
|
2154
|
-
store.generated_at = new Date().toISOString();
|
|
2155
|
-
writeStore2(dir, store);
|
|
2156
|
-
return JSON.stringify({ success: true, count: store.entries.length });
|
|
2157
|
-
}
|
|
2158
|
-
case "update_entry": {
|
|
2159
|
-
if (!args.entry)
|
|
2160
|
-
return JSON.stringify({ error: "entry required" });
|
|
2161
|
-
const idx = store.entries.findIndex((e) => e.path === args.entry.path);
|
|
2162
|
-
const updated = {
|
|
2163
|
-
...args.entry,
|
|
2164
|
-
stability: stabilityLabel(args.entry.churn_score, args.entry.hotfix_count, args.entry.todo_count)
|
|
2165
|
-
};
|
|
2166
|
-
if (idx >= 0) {
|
|
2167
|
-
store.entries[idx] = updated;
|
|
2168
|
-
} else {
|
|
2169
|
-
store.entries.push(updated);
|
|
2170
|
-
}
|
|
2171
|
-
writeStore2(dir, store);
|
|
2172
|
-
return JSON.stringify({ success: true, path: args.entry.path, stability: updated.stability });
|
|
2173
|
-
}
|
|
2174
|
-
case "query_hotspots": {
|
|
2175
|
-
const levels = { stable: 0, moderate: 1, volatile: 2, critical: 3 };
|
|
2176
|
-
const minLevel = levels[args.threshold ?? "volatile"] ?? 2;
|
|
2177
|
-
let results = store.entries.filter((e) => (levels[e.stability] ?? 0) >= minLevel);
|
|
2178
|
-
if (args.path_prefix)
|
|
2179
|
-
results = results.filter((e) => e.path.startsWith(args.path_prefix));
|
|
2180
|
-
results.sort((a, b) => levels[b.stability] - levels[a.stability] || b.churn_score - a.churn_score);
|
|
2181
|
-
if (args.limit)
|
|
2182
|
-
results = results.slice(0, args.limit);
|
|
2183
|
-
return JSON.stringify({ count: results.length, hotspots: results });
|
|
2184
|
-
}
|
|
2185
|
-
}
|
|
2186
|
-
}
|
|
2187
|
-
});
|
|
2188
|
-
|
|
2189
1688
|
// src/tools/policy-engine.ts
|
|
2190
|
-
import { tool as
|
|
2191
|
-
import { readFileSync as
|
|
2192
|
-
import { join as
|
|
1689
|
+
import { tool as tool8 } from "@opencode-ai/plugin";
|
|
1690
|
+
import { readFileSync as readFileSync14, writeFileSync as writeFileSync10, existsSync as existsSync14, mkdirSync as mkdirSync9 } from "fs";
|
|
1691
|
+
import { join as join13 } from "path";
|
|
2193
1692
|
var POLICIES_FILE = "POLICIES.json";
|
|
2194
1693
|
function policiesPath(directory) {
|
|
2195
|
-
return
|
|
1694
|
+
return join13(codebaseDir(directory), POLICIES_FILE);
|
|
2196
1695
|
}
|
|
2197
|
-
function
|
|
1696
|
+
function readStore2(directory) {
|
|
2198
1697
|
const p = policiesPath(directory);
|
|
2199
|
-
if (!
|
|
1698
|
+
if (!existsSync14(p))
|
|
2200
1699
|
return { version: "1.0", last_updated: new Date().toISOString(), policies: [] };
|
|
2201
1700
|
try {
|
|
2202
|
-
return JSON.parse(
|
|
1701
|
+
return JSON.parse(readFileSync14(p, "utf-8"));
|
|
2203
1702
|
} catch {
|
|
2204
1703
|
return { version: "1.0", last_updated: new Date().toISOString(), policies: [] };
|
|
2205
1704
|
}
|
|
2206
1705
|
}
|
|
2207
|
-
function
|
|
1706
|
+
function writeStore2(directory, store) {
|
|
2208
1707
|
const base = codebaseDir(directory);
|
|
2209
|
-
if (!
|
|
2210
|
-
|
|
1708
|
+
if (!existsSync14(base))
|
|
1709
|
+
mkdirSync9(base, { recursive: true });
|
|
2211
1710
|
store.last_updated = new Date().toISOString();
|
|
2212
|
-
|
|
1711
|
+
writeFileSync10(policiesPath(directory), JSON.stringify(store, null, 2), "utf-8");
|
|
2213
1712
|
}
|
|
2214
|
-
var policyEngineTool =
|
|
1713
|
+
var policyEngineTool = tool8({
|
|
2215
1714
|
description: "Self-Healing Policy Engine: manage .codebase/POLICIES.json — add, list, query, toggle, and record violations of editing policies learned from past failures",
|
|
2216
1715
|
args: {
|
|
2217
|
-
action:
|
|
2218
|
-
policy:
|
|
2219
|
-
id:
|
|
2220
|
-
name:
|
|
2221
|
-
trigger:
|
|
2222
|
-
rule:
|
|
2223
|
-
source:
|
|
2224
|
-
failure_count:
|
|
1716
|
+
action: tool8.schema.enum(["list", "add", "record_violation", "toggle", "query"]),
|
|
1717
|
+
policy: tool8.schema.object({
|
|
1718
|
+
id: tool8.schema.string(),
|
|
1719
|
+
name: tool8.schema.string(),
|
|
1720
|
+
trigger: tool8.schema.string(),
|
|
1721
|
+
rule: tool8.schema.string(),
|
|
1722
|
+
source: tool8.schema.enum(["manual", "learned"]),
|
|
1723
|
+
failure_count: tool8.schema.number()
|
|
2225
1724
|
}).optional(),
|
|
2226
|
-
policy_id:
|
|
2227
|
-
active:
|
|
2228
|
-
query:
|
|
2229
|
-
source:
|
|
2230
|
-
active_only:
|
|
2231
|
-
trigger_contains:
|
|
1725
|
+
policy_id: tool8.schema.string().optional(),
|
|
1726
|
+
active: tool8.schema.boolean().optional(),
|
|
1727
|
+
query: tool8.schema.object({
|
|
1728
|
+
source: tool8.schema.enum(["manual", "learned"]).optional(),
|
|
1729
|
+
active_only: tool8.schema.boolean().optional(),
|
|
1730
|
+
trigger_contains: tool8.schema.string().optional()
|
|
2232
1731
|
}).optional()
|
|
2233
1732
|
},
|
|
2234
1733
|
async execute(args, context) {
|
|
2235
1734
|
const dir = context.directory ?? process.cwd();
|
|
2236
|
-
const store =
|
|
1735
|
+
const store = readStore2(dir);
|
|
2237
1736
|
switch (args.action) {
|
|
2238
1737
|
case "list": {
|
|
2239
1738
|
const active = store.policies.filter((p) => p.active);
|
|
@@ -2248,7 +1747,7 @@ var policyEngineTool = tool10({
|
|
|
2248
1747
|
} else {
|
|
2249
1748
|
store.policies.push({ ...args.policy, created_at: new Date().toISOString(), active: true });
|
|
2250
1749
|
}
|
|
2251
|
-
|
|
1750
|
+
writeStore2(dir, store);
|
|
2252
1751
|
return JSON.stringify({ success: true, id: args.policy.id });
|
|
2253
1752
|
}
|
|
2254
1753
|
case "record_violation": {
|
|
@@ -2259,7 +1758,7 @@ var policyEngineTool = tool10({
|
|
|
2259
1758
|
return JSON.stringify({ error: `Policy not found: ${args.policy_id}` });
|
|
2260
1759
|
policy.failure_count++;
|
|
2261
1760
|
policy.last_violated = new Date().toISOString();
|
|
2262
|
-
|
|
1761
|
+
writeStore2(dir, store);
|
|
2263
1762
|
return JSON.stringify({ success: true, policy_id: args.policy_id, failure_count: policy.failure_count });
|
|
2264
1763
|
}
|
|
2265
1764
|
case "toggle": {
|
|
@@ -2269,7 +1768,7 @@ var policyEngineTool = tool10({
|
|
|
2269
1768
|
if (!policy)
|
|
2270
1769
|
return JSON.stringify({ error: `Policy not found: ${args.policy_id}` });
|
|
2271
1770
|
policy.active = args.active !== undefined ? args.active : !policy.active;
|
|
2272
|
-
|
|
1771
|
+
writeStore2(dir, store);
|
|
2273
1772
|
return JSON.stringify({ success: true, policy_id: args.policy_id, active: policy.active });
|
|
2274
1773
|
}
|
|
2275
1774
|
case "query": {
|
|
@@ -2290,22 +1789,22 @@ var policyEngineTool = tool10({
|
|
|
2290
1789
|
});
|
|
2291
1790
|
|
|
2292
1791
|
// src/tools/hash-edit.ts
|
|
2293
|
-
import { tool as
|
|
2294
|
-
import { readFileSync as
|
|
1792
|
+
import { tool as tool9 } from "@opencode-ai/plugin";
|
|
1793
|
+
import { readFileSync as readFileSync15, writeFileSync as writeFileSync11 } from "fs";
|
|
2295
1794
|
import { createHash as createHash2 } from "crypto";
|
|
2296
|
-
var hashEditTool =
|
|
1795
|
+
var hashEditTool = tool9({
|
|
2297
1796
|
description: "Reliable file editing with content verification. Takes a target content, its expected MD5 hash, and replacement content. Only applies if the hash matches, preventing edits on stale file versions.",
|
|
2298
1797
|
args: {
|
|
2299
|
-
filePath:
|
|
2300
|
-
targetContent:
|
|
2301
|
-
expectedHash:
|
|
2302
|
-
replacementContent:
|
|
1798
|
+
filePath: tool9.schema.string(),
|
|
1799
|
+
targetContent: tool9.schema.string(),
|
|
1800
|
+
expectedHash: tool9.schema.string().optional(),
|
|
1801
|
+
replacementContent: tool9.schema.string()
|
|
2303
1802
|
},
|
|
2304
1803
|
async execute(args, context) {
|
|
2305
1804
|
const fullPath = args.filePath.startsWith("/") ? args.filePath : `${context.directory}/${args.filePath}`;
|
|
2306
1805
|
let content;
|
|
2307
1806
|
try {
|
|
2308
|
-
content =
|
|
1807
|
+
content = readFileSync15(fullPath, "utf-8");
|
|
2309
1808
|
} catch (e) {
|
|
2310
1809
|
return `Error: Could not read file ${args.filePath}`;
|
|
2311
1810
|
}
|
|
@@ -2319,17 +1818,17 @@ var hashEditTool = tool11({
|
|
|
2319
1818
|
}
|
|
2320
1819
|
}
|
|
2321
1820
|
const newContent = content.replace(args.targetContent, args.replacementContent);
|
|
2322
|
-
|
|
1821
|
+
writeFileSync11(fullPath, newContent, "utf-8");
|
|
2323
1822
|
return `Successfully updated ${args.filePath} using hash-anchored edit.`;
|
|
2324
1823
|
}
|
|
2325
1824
|
});
|
|
2326
1825
|
|
|
2327
1826
|
// src/tools/council.ts
|
|
2328
|
-
import { tool as
|
|
2329
|
-
import { appendFileSync as appendFileSync3, existsSync as
|
|
2330
|
-
import { join as
|
|
1827
|
+
import { tool as tool10 } from "@opencode-ai/plugin";
|
|
1828
|
+
import { appendFileSync as appendFileSync3, existsSync as existsSync15, mkdirSync as mkdirSync10 } from "fs";
|
|
1829
|
+
import { join as join14 } from "path";
|
|
2331
1830
|
import { createHash as createHash3 } from "crypto";
|
|
2332
|
-
import { readFileSync as
|
|
1831
|
+
import { readFileSync as readFileSync16 } from "fs";
|
|
2333
1832
|
var _councilCache = new Map;
|
|
2334
1833
|
var COUNCIL_CACHE_TTL_MS = 20 * 60 * 1000;
|
|
2335
1834
|
function councilCacheKey(task, agents, stateVersion, indexVersion) {
|
|
@@ -2350,20 +1849,20 @@ async function runWithConcurrencyLimit(tasks, limit) {
|
|
|
2350
1849
|
return results;
|
|
2351
1850
|
}
|
|
2352
1851
|
function createCouncilTool(client) {
|
|
2353
|
-
return
|
|
1852
|
+
return tool10({
|
|
2354
1853
|
description: "Run an ensemble of agents (Council) on the same task to reach consensus or compare approaches. Runs specialized agents in parallel (bounded concurrency) and returns their synthesized outputs.",
|
|
2355
1854
|
args: {
|
|
2356
|
-
task:
|
|
2357
|
-
agents:
|
|
2358
|
-
force_fresh:
|
|
2359
|
-
max_concurrency:
|
|
1855
|
+
task: tool10.schema.string(),
|
|
1856
|
+
agents: tool10.schema.array(tool10.schema.string()).optional(),
|
|
1857
|
+
force_fresh: tool10.schema.boolean().optional().default(false),
|
|
1858
|
+
max_concurrency: tool10.schema.number().optional().default(3)
|
|
2360
1859
|
},
|
|
2361
1860
|
async execute(args, context) {
|
|
2362
1861
|
const agents = args.agents || ["architect", "reviewer", "backend-coder"];
|
|
2363
1862
|
const concurrencyLimit = Math.max(1, Math.min(5, typeof args.max_concurrency === "number" ? args.max_concurrency : 3));
|
|
2364
1863
|
const index = readCodebaseIndex(context.directory);
|
|
2365
1864
|
const sp = statePath(context.directory);
|
|
2366
|
-
const rawState =
|
|
1865
|
+
const rawState = existsSync15(sp) ? readFileSync16(sp, "utf-8") : "";
|
|
2367
1866
|
const state = rawState ? parseState(rawState) : {};
|
|
2368
1867
|
const stateVersion = typeof state.summaryVersion === "number" ? state.summaryVersion : 0;
|
|
2369
1868
|
const indexVersion = typeof index.summaryVersion === "number" ? index.summaryVersion : 0;
|
|
@@ -2439,117 +1938,18 @@ Please synthesize these results. Identify areas of agreement, resolve conflicts,
|
|
|
2439
1938
|
function persistCouncilResult(directory, payload) {
|
|
2440
1939
|
try {
|
|
2441
1940
|
const base = codebaseDir(directory);
|
|
2442
|
-
if (!
|
|
2443
|
-
|
|
2444
|
-
const path =
|
|
1941
|
+
if (!existsSync15(base))
|
|
1942
|
+
mkdirSync10(base, { recursive: true });
|
|
1943
|
+
const path = join14(base, "COUNCILS.jsonl");
|
|
2445
1944
|
appendFileSync3(path, JSON.stringify(payload) + `
|
|
2446
1945
|
`, "utf-8");
|
|
2447
1946
|
} catch {}
|
|
2448
1947
|
}
|
|
2449
1948
|
|
|
2450
|
-
// src/tools/context-generator.ts
|
|
2451
|
-
import { tool as tool13 } from "@opencode-ai/plugin";
|
|
2452
|
-
import { writeFileSync as writeFileSync14, existsSync as existsSync18, readFileSync as readFileSync19, readdirSync as readdirSync4, statSync as statSync2 } from "fs";
|
|
2453
|
-
import { join as join17 } from "path";
|
|
2454
|
-
var contextGeneratorTool = tool13({
|
|
2455
|
-
description: "Auto-generate or update hierarchical context files (AGENTS.md, CLAUDE.md) throughout the project. These files provide critical grounding for AI agents.",
|
|
2456
|
-
args: {
|
|
2457
|
-
targetDir: tool13.schema.string().optional(),
|
|
2458
|
-
force: tool13.schema.boolean().optional()
|
|
2459
|
-
},
|
|
2460
|
-
async execute(args, context) {
|
|
2461
|
-
const root = context.directory;
|
|
2462
|
-
const target = args.targetDir ? join17(root, args.targetDir) : root;
|
|
2463
|
-
if (!existsSync18(target)) {
|
|
2464
|
-
return `Error: Directory ${target} does not exist.`;
|
|
2465
|
-
}
|
|
2466
|
-
const agentsMdPath = join17(target, "AGENTS.md");
|
|
2467
|
-
if (existsSync18(agentsMdPath) && !args.force) {
|
|
2468
|
-
return `AGENTS.md already exists in ${target}. Use force: true to overwrite.`;
|
|
2469
|
-
}
|
|
2470
|
-
const pkgPath = join17(root, "package.json");
|
|
2471
|
-
let projectName = "Project";
|
|
2472
|
-
let techStack = "Unknown";
|
|
2473
|
-
if (existsSync18(pkgPath)) {
|
|
2474
|
-
try {
|
|
2475
|
-
const pkg = JSON.parse(readFileSync19(pkgPath, "utf-8"));
|
|
2476
|
-
projectName = pkg.name || projectName;
|
|
2477
|
-
techStack = Object.keys(pkg.dependencies || {}).slice(0, 5).join(", ");
|
|
2478
|
-
} catch {}
|
|
2479
|
-
}
|
|
2480
|
-
const content = `# AGENTS.md for ${projectName}
|
|
2481
|
-
|
|
2482
|
-
## Context
|
|
2483
|
-
- **Tech Stack**: ${techStack}
|
|
2484
|
-
- **Primary Goal**: [Explain the main purpose of this directory/project]
|
|
2485
|
-
|
|
2486
|
-
## Rules for Agents
|
|
2487
|
-
1. **Consistency**: Follow existing patterns in this directory.
|
|
2488
|
-
2. **Safety**: Do not modify files in \`node_modules\` or other sensitive areas.
|
|
2489
|
-
3. **Planning**: Always check \`.planning/STATE.md\` before executing major changes.
|
|
2490
|
-
|
|
2491
|
-
## Directory Map
|
|
2492
|
-
${readdirSync4(target).slice(0, 10).map((f) => {
|
|
2493
|
-
const s = statSync2(join17(target, f));
|
|
2494
|
-
return `- \`${f}\`${s.isDirectory() ? "/" : ""} : [Description]`;
|
|
2495
|
-
}).join(`
|
|
2496
|
-
`)}
|
|
2497
|
-
|
|
2498
|
-
---
|
|
2499
|
-
Generated by FlowDeck Context Generator.
|
|
2500
|
-
`;
|
|
2501
|
-
writeFileSync14(agentsMdPath, content, "utf-8");
|
|
2502
|
-
return `Successfully generated AGENTS.md in ${target}.`;
|
|
2503
|
-
}
|
|
2504
|
-
});
|
|
2505
|
-
|
|
2506
|
-
// src/tools/create-skill.ts
|
|
2507
|
-
import { tool as tool14 } from "@opencode-ai/plugin";
|
|
2508
|
-
import { mkdirSync as mkdirSync12, writeFileSync as writeFileSync15, existsSync as existsSync19 } from "fs";
|
|
2509
|
-
import { join as join18, dirname as dirname3 } from "path";
|
|
2510
|
-
import { fileURLToPath } from "url";
|
|
2511
|
-
var SKILLS_DIR = join18(dirname3(fileURLToPath(import.meta.url)), "..", "skills");
|
|
2512
|
-
var createSkillTool = tool14({
|
|
2513
|
-
description: "Create a new reusable skill in the FlowDeck skill library (src/skills/). " + "Use this when you discover a repeatable pattern, solve a novel problem with human guidance, " + "or want to capture domain knowledge for future sessions.",
|
|
2514
|
-
args: {
|
|
2515
|
-
name: tool14.schema.string().describe("Unique kebab-case skill name, e.g. 'api-rate-limiting'"),
|
|
2516
|
-
description: tool14.schema.string().describe("One-sentence description of what this skill does"),
|
|
2517
|
-
content: tool14.schema.string().describe("Full skill body in Markdown. Must include: ## When to Activate, ## Steps, and ## Examples sections."),
|
|
2518
|
-
tags: tool14.schema.array(tool14.schema.string()).optional().describe("Optional tags for categorisation, e.g. ['performance', 'typescript']")
|
|
2519
|
-
},
|
|
2520
|
-
async execute(args) {
|
|
2521
|
-
const skillDir = join18(SKILLS_DIR, args.name);
|
|
2522
|
-
const skillFile = join18(skillDir, "SKILL.md");
|
|
2523
|
-
if (existsSync19(skillFile)) {
|
|
2524
|
-
return `Skill '${args.name}' already exists at ${skillFile}.
|
|
2525
|
-
` + `Use a different name or delete the existing skill directory first.`;
|
|
2526
|
-
}
|
|
2527
|
-
const tagLine = args.tags?.length ? `
|
|
2528
|
-
tags: [${args.tags.join(", ")}]` : "";
|
|
2529
|
-
const frontmatter = `---
|
|
2530
|
-
name: ${args.name}
|
|
2531
|
-
description: ${args.description}
|
|
2532
|
-
origin: FlowDeck (self-learned)${tagLine}
|
|
2533
|
-
---
|
|
2534
|
-
|
|
2535
|
-
`;
|
|
2536
|
-
const fullContent = frontmatter + args.content.trimStart();
|
|
2537
|
-
try {
|
|
2538
|
-
mkdirSync12(skillDir, { recursive: true });
|
|
2539
|
-
writeFileSync15(skillFile, fullContent, "utf-8");
|
|
2540
|
-
return `✓ Skill '${args.name}' created at ${skillFile}
|
|
2541
|
-
|
|
2542
|
-
` + `The skill is now part of the FlowDeck library. Restart OpenCode to load it into the active session.`;
|
|
2543
|
-
} catch (err) {
|
|
2544
|
-
return `Error creating skill '${args.name}': ${err.message}`;
|
|
2545
|
-
}
|
|
2546
|
-
}
|
|
2547
|
-
});
|
|
2548
|
-
|
|
2549
1949
|
// src/tools/reflect.ts
|
|
2550
|
-
import { tool as
|
|
2551
|
-
import { existsSync as
|
|
2552
|
-
import { join as
|
|
1950
|
+
import { tool as tool11 } from "@opencode-ai/plugin";
|
|
1951
|
+
import { existsSync as existsSync16, readFileSync as readFileSync17 } from "fs";
|
|
1952
|
+
import { join as join15 } from "path";
|
|
2553
1953
|
var MAX_ARTIFACT_BYTES = 4000;
|
|
2554
1954
|
function tail(text, maxBytes) {
|
|
2555
1955
|
if (text.length <= maxBytes)
|
|
@@ -2557,10 +1957,10 @@ function tail(text, maxBytes) {
|
|
|
2557
1957
|
return `... (truncated) ...
|
|
2558
1958
|
` + text.slice(-maxBytes);
|
|
2559
1959
|
}
|
|
2560
|
-
var reflectTool =
|
|
1960
|
+
var reflectTool = tool11({
|
|
2561
1961
|
description: "Gather session artifacts (decisions, telemetry, failures, policies) and return a structured " + "reflection context that the agent can reason over to produce self-improvement proposals.",
|
|
2562
1962
|
args: {
|
|
2563
|
-
scope:
|
|
1963
|
+
scope: tool11.schema.enum(["session", "project"]).optional().describe("'session' (default) uses only recent artifacts; 'project' includes all historical data")
|
|
2564
1964
|
},
|
|
2565
1965
|
async execute(args, context) {
|
|
2566
1966
|
const root = context.directory;
|
|
@@ -2578,11 +1978,11 @@ var reflectTool = tool15({
|
|
|
2578
1978
|
];
|
|
2579
1979
|
let found = 0;
|
|
2580
1980
|
for (const [rel, label] of ARTIFACT_PATHS) {
|
|
2581
|
-
const full =
|
|
2582
|
-
if (!
|
|
1981
|
+
const full = join15(root, rel);
|
|
1982
|
+
if (!existsSync16(full))
|
|
2583
1983
|
continue;
|
|
2584
1984
|
try {
|
|
2585
|
-
const raw =
|
|
1985
|
+
const raw = readFileSync17(full, "utf-8").trim();
|
|
2586
1986
|
if (!raw)
|
|
2587
1987
|
continue;
|
|
2588
1988
|
const count = raw.split(`
|
|
@@ -2595,23 +1995,23 @@ var reflectTool = tool15({
|
|
|
2595
1995
|
return `No FlowDeck artifacts found under .codebase/.
|
|
2596
1996
|
` + "Run some tasks first so decisions, telemetry, and failures are recorded.";
|
|
2597
1997
|
}
|
|
2598
|
-
sections.push("## What to do with this data", "Analyse the artifacts above and:", "1. **Identify patterns** — repeated tool sequences, recurring failure modes", "2. **Surface gaps** — knowledge or skills that were missing and had to be figured out", "3. **Propose improvements** — for each gap or pattern, either:", " -
|
|
1998
|
+
sections.push("## What to do with this data", "Analyse the artifacts above and:", "1. **Identify patterns** — repeated tool sequences, recurring failure modes", "2. **Surface gaps** — knowledge or skills that were missing and had to be figured out", "3. **Propose improvements** — for each gap or pattern, either:", " - Write a new skill markdown file under `src/skills/<name>/SKILL.md`, OR", " - Propose a new entry in `.codebase/POLICIES.json`", "4. **Summarise** — 3–5 bullet points of the most impactful takeaways");
|
|
2599
1999
|
return sections.join(`
|
|
2600
2000
|
`);
|
|
2601
2001
|
}
|
|
2602
2002
|
});
|
|
2603
2003
|
|
|
2604
2004
|
// src/tools/codegraph-tool.ts
|
|
2605
|
-
import { tool as
|
|
2005
|
+
import { tool as tool12 } from "@opencode-ai/plugin";
|
|
2606
2006
|
|
|
2607
2007
|
// src/services/codegraph.ts
|
|
2608
2008
|
import { spawnSync } from "child_process";
|
|
2609
|
-
import { existsSync as
|
|
2610
|
-
import { join as
|
|
2009
|
+
import { existsSync as existsSync17, readFileSync as readFileSync18, writeFileSync as writeFileSync12, mkdirSync as mkdirSync11 } from "fs";
|
|
2010
|
+
import { join as join16 } from "path";
|
|
2611
2011
|
var CODEGRAPH_META_FILE = "CODEGRAPH.md";
|
|
2612
2012
|
var MAX_FRESHNESS_MS = 30 * 60 * 1000;
|
|
2613
2013
|
function metaPath(dir) {
|
|
2614
|
-
return
|
|
2014
|
+
return join16(codebaseDir(dir), CODEGRAPH_META_FILE);
|
|
2615
2015
|
}
|
|
2616
2016
|
function isCodegraphInstalled() {
|
|
2617
2017
|
try {
|
|
@@ -2626,11 +2026,11 @@ function isCodegraphInstalled() {
|
|
|
2626
2026
|
}
|
|
2627
2027
|
}
|
|
2628
2028
|
function isCodegraphIndexed(dir) {
|
|
2629
|
-
return
|
|
2029
|
+
return existsSync17(join16(dir, ".codegraph", "codegraph.db"));
|
|
2630
2030
|
}
|
|
2631
2031
|
function readCodegraphMeta(dir) {
|
|
2632
2032
|
const path = metaPath(dir);
|
|
2633
|
-
if (!
|
|
2033
|
+
if (!existsSync17(path)) {
|
|
2634
2034
|
return {
|
|
2635
2035
|
installed: false,
|
|
2636
2036
|
indexed: false,
|
|
@@ -2643,7 +2043,7 @@ function readCodegraphMeta(dir) {
|
|
|
2643
2043
|
};
|
|
2644
2044
|
}
|
|
2645
2045
|
try {
|
|
2646
|
-
const content =
|
|
2046
|
+
const content = readFileSync18(path, "utf-8");
|
|
2647
2047
|
return parseCodegraphMeta(content);
|
|
2648
2048
|
} catch {
|
|
2649
2049
|
return {
|
|
@@ -2710,8 +2110,8 @@ function parseCodegraphMeta(content) {
|
|
|
2710
2110
|
}
|
|
2711
2111
|
function writeCodegraphMeta(dir, meta) {
|
|
2712
2112
|
const base = codebaseDir(dir);
|
|
2713
|
-
if (!
|
|
2714
|
-
|
|
2113
|
+
if (!existsSync17(base))
|
|
2114
|
+
mkdirSync11(base, { recursive: true });
|
|
2715
2115
|
const lines = [
|
|
2716
2116
|
"# Codegraph Metadata",
|
|
2717
2117
|
"",
|
|
@@ -2724,7 +2124,7 @@ function writeCodegraphMeta(dir, meta) {
|
|
|
2724
2124
|
`**installLog:** ${meta.installLog}`,
|
|
2725
2125
|
`**indexLog:** ${meta.indexLog}`
|
|
2726
2126
|
];
|
|
2727
|
-
|
|
2127
|
+
writeFileSync12(metaPath(dir), lines.join(`
|
|
2728
2128
|
`), "utf-8");
|
|
2729
2129
|
}
|
|
2730
2130
|
function isCodegraphFresh(dir, maxAgeMs = MAX_FRESHNESS_MS) {
|
|
@@ -2935,11 +2335,11 @@ function markCodegraphStale(dir) {
|
|
|
2935
2335
|
}
|
|
2936
2336
|
|
|
2937
2337
|
// src/tools/codegraph-tool.ts
|
|
2938
|
-
var codegraphTool =
|
|
2338
|
+
var codegraphTool = tool12({
|
|
2939
2339
|
description: "Manage codegraph lifecycle only: check installation, install, init/rebuild the index, refresh (incremental sync), " + "query status, or mark-stale. Valid actions: check | install | init | refresh | status | mark-stale. " + "Do NOT use this tool for code intelligence queries (files, search, callers, callees, etc.) — " + "those are available as codegraph MCP tools (codegraph_files, codegraph_search, codegraph_context, " + "codegraph_explore, codegraph_callers, codegraph_callees, codegraph_impact, codegraph_trace) " + "when the index is ready.",
|
|
2940
2340
|
args: {
|
|
2941
|
-
action:
|
|
2942
|
-
agent:
|
|
2341
|
+
action: tool12.schema.enum(["check", "install", "init", "refresh", "status", "mark-stale"]),
|
|
2342
|
+
agent: tool12.schema.string().optional()
|
|
2943
2343
|
},
|
|
2944
2344
|
async execute(args, context) {
|
|
2945
2345
|
const dir = context.directory ?? process.cwd();
|
|
@@ -3028,21 +2428,21 @@ var codegraphTool = tool16({
|
|
|
3028
2428
|
});
|
|
3029
2429
|
|
|
3030
2430
|
// src/tools/load-rules.ts
|
|
3031
|
-
import { tool as
|
|
3032
|
-
import { existsSync as
|
|
3033
|
-
import { join as
|
|
3034
|
-
import { fileURLToPath
|
|
3035
|
-
var RULES_DIR =
|
|
2431
|
+
import { tool as tool13 } from "@opencode-ai/plugin";
|
|
2432
|
+
import { existsSync as existsSync18, readFileSync as readFileSync19 } from "fs";
|
|
2433
|
+
import { join as join17, dirname as dirname2 } from "path";
|
|
2434
|
+
import { fileURLToPath } from "url";
|
|
2435
|
+
var RULES_DIR = join17(dirname2(fileURLToPath(import.meta.url)), "..", "rules");
|
|
3036
2436
|
var _loadedPaths = new Set;
|
|
3037
|
-
var loadRulesTool =
|
|
2437
|
+
var loadRulesTool = tool13({
|
|
3038
2438
|
description: "Load additional rule modules on demand for the current workflow stage. " + "Use this at the start of a new stage (execute, verify, fix-bug) to load " + "coding-style, security, testing, and language-specific rules that were not " + "injected at startup. Returns the full text of selected rules. " + "Already-loaded rules are not returned again (suppressed to avoid duplication).",
|
|
3039
2439
|
args: {
|
|
3040
|
-
stage:
|
|
3041
|
-
languages:
|
|
3042
|
-
force_reload:
|
|
2440
|
+
stage: tool13.schema.string().optional().describe("Current workflow stage: discuss | plan | execute | verify | fix-bug | write-docs"),
|
|
2441
|
+
languages: tool13.schema.array(tool13.schema.string()).optional().describe("Project languages to load rules for, e.g. ['typescript']. " + "Omit to use all languages (returns all matching stage rules)."),
|
|
2442
|
+
force_reload: tool13.schema.boolean().optional().default(false).describe("When true, return rules even if they were already loaded in this session. " + "Use only when stage context has changed and you need a fresh load.")
|
|
3043
2443
|
},
|
|
3044
2444
|
async execute(args) {
|
|
3045
|
-
const rulesDir =
|
|
2445
|
+
const rulesDir = existsSync18(RULES_DIR) ? RULES_DIR : null;
|
|
3046
2446
|
if (!rulesDir) {
|
|
3047
2447
|
return JSON.stringify({
|
|
3048
2448
|
loaded: [],
|
|
@@ -3068,7 +2468,7 @@ var loadRulesTool = tool17({
|
|
|
3068
2468
|
continue;
|
|
3069
2469
|
}
|
|
3070
2470
|
try {
|
|
3071
|
-
const text =
|
|
2471
|
+
const text = readFileSync19(rule.path, "utf-8");
|
|
3072
2472
|
contents.push(`## ${name}
|
|
3073
2473
|
|
|
3074
2474
|
${text}`);
|
|
@@ -3099,11 +2499,11 @@ ${text}`);
|
|
|
3099
2499
|
function ruleShortName(rule) {
|
|
3100
2500
|
return rule.path.replace(RULES_DIR + "/", "").replace(/\.md$/, "");
|
|
3101
2501
|
}
|
|
3102
|
-
var listRulesTool =
|
|
2502
|
+
var listRulesTool = tool13({
|
|
3103
2503
|
description: "List all available FlowDeck rule modules with their metadata (description, always_on, " + "stages, languages). Use this before calling load-rules to see what is available. " + "Does NOT load rule content — only returns metadata for discovery.",
|
|
3104
2504
|
args: {},
|
|
3105
2505
|
async execute() {
|
|
3106
|
-
const rulesDir =
|
|
2506
|
+
const rulesDir = existsSync18(RULES_DIR) ? RULES_DIR : null;
|
|
3107
2507
|
if (!rulesDir) {
|
|
3108
2508
|
return JSON.stringify({ rules: [], error: `Rules directory not found at ${RULES_DIR}` });
|
|
3109
2509
|
}
|
|
@@ -3123,13 +2523,13 @@ var listRulesTool = tool17({
|
|
|
3123
2523
|
});
|
|
3124
2524
|
|
|
3125
2525
|
// src/tools/rtk-setup.ts
|
|
3126
|
-
import { tool as
|
|
2526
|
+
import { tool as tool14 } from "@opencode-ai/plugin";
|
|
3127
2527
|
|
|
3128
2528
|
// src/services/rtk-manager.ts
|
|
3129
2529
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
3130
|
-
import { existsSync as
|
|
2530
|
+
import { existsSync as existsSync19 } from "fs";
|
|
3131
2531
|
import { homedir as homedir2 } from "os";
|
|
3132
|
-
import { join as
|
|
2532
|
+
import { join as join18 } from "path";
|
|
3133
2533
|
|
|
3134
2534
|
// src/services/rtk-policy.ts
|
|
3135
2535
|
var SUPPORTED_COMMANDS = new Set([
|
|
@@ -3175,7 +2575,7 @@ var INSTALL_INSTRUCTIONS = [
|
|
|
3175
2575
|
"After installation, call rtk-setup again to verify detection."
|
|
3176
2576
|
].join(`
|
|
3177
2577
|
`);
|
|
3178
|
-
var CANDIDATE_PATHS = [
|
|
2578
|
+
var CANDIDATE_PATHS = [join18(homedir2(), ".local", "bin", "rtk"), "/usr/local/bin/rtk", "/usr/bin/rtk"];
|
|
3179
2579
|
function detectRtk() {
|
|
3180
2580
|
const fromPath = spawnSync2("rtk", ["--version"], { encoding: "utf-8", timeout: 5000 });
|
|
3181
2581
|
if (fromPath.status === 0) {
|
|
@@ -3184,7 +2584,7 @@ function detectRtk() {
|
|
|
3184
2584
|
return { installed: true, binPath: "rtk", version };
|
|
3185
2585
|
}
|
|
3186
2586
|
for (const candidate of CANDIDATE_PATHS) {
|
|
3187
|
-
if (!
|
|
2587
|
+
if (!existsSync19(candidate))
|
|
3188
2588
|
continue;
|
|
3189
2589
|
const result = spawnSync2(candidate, ["--version"], { encoding: "utf-8", timeout: 5000 });
|
|
3190
2590
|
if (result.status === 0) {
|
|
@@ -3262,7 +2662,7 @@ function getRtkStatus(opts) {
|
|
|
3262
2662
|
}
|
|
3263
2663
|
|
|
3264
2664
|
// src/tools/rtk-setup.ts
|
|
3265
|
-
var rtkSetupTool =
|
|
2665
|
+
var rtkSetupTool = tool14({
|
|
3266
2666
|
description: [
|
|
3267
2667
|
"Detect, initialize, and report status of rtk (output compression proxy for CLI commands).",
|
|
3268
2668
|
"rtk reduces noisy CLI output (git, npm, test runners, linters, docker) by 60-90%.",
|
|
@@ -3270,7 +2670,7 @@ var rtkSetupTool = tool18({
|
|
|
3270
2670
|
"When RTK_INSTALLED=true in the environment, use `$RTK_BIN git status` for compressed output."
|
|
3271
2671
|
].join(" "),
|
|
3272
2672
|
args: {
|
|
3273
|
-
action:
|
|
2673
|
+
action: tool14.schema.enum(["status", "init"]).optional().describe("'status' — detect and report rtk state (default). " + "'init' — detect, then run `rtk init -g` to install the bash hook. " + "Use 'init' only once per environment setup.")
|
|
3274
2674
|
},
|
|
3275
2675
|
async execute(args) {
|
|
3276
2676
|
const action = args.action ?? "status";
|
|
@@ -3310,15 +2710,15 @@ var rtkSetupTool = tool18({
|
|
|
3310
2710
|
});
|
|
3311
2711
|
|
|
3312
2712
|
// src/hooks/guard-rails.ts
|
|
3313
|
-
import { existsSync as
|
|
3314
|
-
import { join as
|
|
2713
|
+
import { existsSync as existsSync20, readFileSync as readFileSync20 } from "fs";
|
|
2714
|
+
import { join as join19 } from "path";
|
|
3315
2715
|
var PLANNING_DIR2 = ".planning";
|
|
3316
2716
|
var CONFIG_FILE = "config.json";
|
|
3317
2717
|
var STATE_FILE2 = "STATE.md";
|
|
3318
2718
|
function resolveExecutionMode(configPath, trustScore, volatility) {
|
|
3319
|
-
if (
|
|
2719
|
+
if (existsSync20(configPath)) {
|
|
3320
2720
|
try {
|
|
3321
|
-
const config = JSON.parse(
|
|
2721
|
+
const config = JSON.parse(readFileSync20(configPath, "utf-8"));
|
|
3322
2722
|
if (config.execution_mode === "review-only")
|
|
3323
2723
|
return "review-only";
|
|
3324
2724
|
if (config.execution_mode === "guarded")
|
|
@@ -3372,22 +2772,22 @@ async function guardRailsHook(ctx, input, _output) {
|
|
|
3372
2772
|
if (!ENABLED)
|
|
3373
2773
|
return;
|
|
3374
2774
|
const dir = ctx.directory;
|
|
3375
|
-
const planningDirPath =
|
|
2775
|
+
const planningDirPath = join19(dir, PLANNING_DIR2);
|
|
3376
2776
|
const codebaseDirectory = codebaseDir(dir);
|
|
3377
|
-
const configPath =
|
|
3378
|
-
const statePath2 =
|
|
2777
|
+
const configPath = join19(planningDirPath, CONFIG_FILE);
|
|
2778
|
+
const statePath2 = join19(planningDirPath, STATE_FILE2);
|
|
3379
2779
|
const workspaceRoot = findWorkspaceRoot(dir);
|
|
3380
2780
|
if (workspaceRoot && dir !== workspaceRoot) {
|
|
3381
2781
|
const config = getWorkspaceConfig(dir);
|
|
3382
|
-
if (config && config.workspace_mode === "shared" && !
|
|
2782
|
+
if (config && config.workspace_mode === "shared" && !existsSync20(planningDirPath)) {
|
|
3383
2783
|
const msg = `No .planning/ in this sub-repo. Switch to workspace root: cd ${workspaceRoot}`;
|
|
3384
2784
|
throw new Error(`[flowdeck] BLOCK: ${msg}`);
|
|
3385
2785
|
}
|
|
3386
2786
|
}
|
|
3387
2787
|
if (input.tool === "write" || input.tool === "edit") {
|
|
3388
|
-
if (!
|
|
2788
|
+
if (!existsSync20(planningDirPath))
|
|
3389
2789
|
return;
|
|
3390
|
-
if (!
|
|
2790
|
+
if (!existsSync20(codebaseDirectory)) {
|
|
3391
2791
|
throw new Error(`[flowdeck] WARNING: .codebase/ not found. Run /fd-map-codebase to map the codebase.`);
|
|
3392
2792
|
}
|
|
3393
2793
|
const execMode = resolveExecutionMode(configPath, null);
|
|
@@ -3443,15 +2843,15 @@ function getDesignGateMessage(dir) {
|
|
|
3443
2843
|
}
|
|
3444
2844
|
function planSuggestsUiHeavy(dir, phase) {
|
|
3445
2845
|
const planPath = phasePlanPath(dir, phase);
|
|
3446
|
-
if (!
|
|
2846
|
+
if (!existsSync20(planPath))
|
|
3447
2847
|
return false;
|
|
3448
|
-
const planContent =
|
|
2848
|
+
const planContent = readFileSync20(planPath, "utf-8");
|
|
3449
2849
|
return isUiHeavyTask(planContent);
|
|
3450
2850
|
}
|
|
3451
2851
|
function effectiveSeverity(configPath, statePath2) {
|
|
3452
|
-
if (
|
|
2852
|
+
if (existsSync20(configPath)) {
|
|
3453
2853
|
try {
|
|
3454
|
-
const configContent =
|
|
2854
|
+
const configContent = readFileSync20(configPath, "utf-8");
|
|
3455
2855
|
const config = JSON.parse(configContent);
|
|
3456
2856
|
if (config.guard_enforcement === "warn")
|
|
3457
2857
|
return "warn";
|
|
@@ -3467,10 +2867,10 @@ function getEffectiveSeverity(configPath, statePath2) {
|
|
|
3467
2867
|
return effectiveSeverity(configPath, statePath2);
|
|
3468
2868
|
}
|
|
3469
2869
|
function getPlanConfirmed(statePath2) {
|
|
3470
|
-
if (!
|
|
2870
|
+
if (!existsSync20(statePath2))
|
|
3471
2871
|
return false;
|
|
3472
2872
|
try {
|
|
3473
|
-
const content =
|
|
2873
|
+
const content = readFileSync20(statePath2, "utf-8");
|
|
3474
2874
|
const match = content.match(/plan_confirmed:\s*(true|false)/i);
|
|
3475
2875
|
return match ? match[1].toLowerCase() === "true" : false;
|
|
3476
2876
|
} catch {
|
|
@@ -3478,32 +2878,32 @@ function getPlanConfirmed(statePath2) {
|
|
|
3478
2878
|
}
|
|
3479
2879
|
}
|
|
3480
2880
|
function getWarningMessage(planningDir2) {
|
|
3481
|
-
if (!
|
|
2881
|
+
if (!existsSync20(join19(planningDir2, STATE_FILE2))) {
|
|
3482
2882
|
return "No STATE.md found. Run /fd-map-codebase then /fd-new-feature to start a feature.";
|
|
3483
2883
|
}
|
|
3484
2884
|
return "Plan not confirmed. Run /fd-plan and confirm to enable execution.";
|
|
3485
2885
|
}
|
|
3486
2886
|
function getBlockMessage(planningDir2) {
|
|
3487
|
-
if (!
|
|
2887
|
+
if (!existsSync20(join19(planningDir2, STATE_FILE2))) {
|
|
3488
2888
|
return "No STATE.md found. Run /fd-map-codebase then /fd-new-feature to start a feature.";
|
|
3489
2889
|
}
|
|
3490
2890
|
return "Plan not confirmed. Run /fd-plan and confirm to enable execution.";
|
|
3491
2891
|
}
|
|
3492
2892
|
|
|
3493
2893
|
// src/hooks/tool-guard.ts
|
|
3494
|
-
import { existsSync as
|
|
3495
|
-
import { join as
|
|
2894
|
+
import { existsSync as existsSync21, readFileSync as readFileSync21 } from "fs";
|
|
2895
|
+
import { join as join20 } from "path";
|
|
3496
2896
|
var IS_ENABLED = () => process.env.FLOWDECK_TOOL_GUARD_ENABLED === "on";
|
|
3497
2897
|
var BLOCKED_PATTERNS = {
|
|
3498
2898
|
read: [".env", ".pem", ".key", ".secret"],
|
|
3499
2899
|
write: ["node_modules"],
|
|
3500
2900
|
bash: ["rm -rf"]
|
|
3501
2901
|
};
|
|
3502
|
-
function isBlocked(
|
|
3503
|
-
const patterns = BLOCKED_PATTERNS[
|
|
2902
|
+
function isBlocked(tool15, args) {
|
|
2903
|
+
const patterns = BLOCKED_PATTERNS[tool15];
|
|
3504
2904
|
if (!patterns)
|
|
3505
2905
|
return null;
|
|
3506
|
-
if (
|
|
2906
|
+
if (tool15 === "bash") {
|
|
3507
2907
|
const cmd = args.command;
|
|
3508
2908
|
if (!cmd)
|
|
3509
2909
|
return null;
|
|
@@ -3514,7 +2914,7 @@ function isBlocked(tool19, args) {
|
|
|
3514
2914
|
}
|
|
3515
2915
|
return null;
|
|
3516
2916
|
}
|
|
3517
|
-
if (
|
|
2917
|
+
if (tool15 === "read") {
|
|
3518
2918
|
const filePath = args.filePath;
|
|
3519
2919
|
if (!filePath)
|
|
3520
2920
|
return null;
|
|
@@ -3525,7 +2925,7 @@ function isBlocked(tool19, args) {
|
|
|
3525
2925
|
}
|
|
3526
2926
|
return null;
|
|
3527
2927
|
}
|
|
3528
|
-
if (
|
|
2928
|
+
if (tool15 === "write") {
|
|
3529
2929
|
const filePath = args.filePath;
|
|
3530
2930
|
if (!filePath)
|
|
3531
2931
|
return null;
|
|
@@ -3539,11 +2939,11 @@ function isBlocked(tool19, args) {
|
|
|
3539
2939
|
return null;
|
|
3540
2940
|
}
|
|
3541
2941
|
function checkArchConstraint(directory, filePath) {
|
|
3542
|
-
const constraintsPath =
|
|
3543
|
-
if (!
|
|
2942
|
+
const constraintsPath = join20(codebaseDir(directory), "CONSTRAINTS.md");
|
|
2943
|
+
if (!existsSync21(constraintsPath))
|
|
3544
2944
|
return null;
|
|
3545
2945
|
try {
|
|
3546
|
-
const content =
|
|
2946
|
+
const content = readFileSync21(constraintsPath, "utf-8");
|
|
3547
2947
|
const match = content.match(/## Forbidden Paths\n([\s\S]*?)(?:\n##|$)/);
|
|
3548
2948
|
if (!match)
|
|
3549
2949
|
return null;
|
|
@@ -3584,9 +2984,9 @@ function isUiDesignApprovalRequired(directory) {
|
|
|
3584
2984
|
return !(state.design_stage === "handoff_complete" && state.design_approved);
|
|
3585
2985
|
}
|
|
3586
2986
|
const planPath = phasePlanPath(directory, state.phase || 1);
|
|
3587
|
-
if (!
|
|
2987
|
+
if (!existsSync21(planPath))
|
|
3588
2988
|
return false;
|
|
3589
|
-
const planContent =
|
|
2989
|
+
const planContent = readFileSync21(planPath, "utf-8");
|
|
3590
2990
|
if (!isUiHeavyTask(planContent))
|
|
3591
2991
|
return false;
|
|
3592
2992
|
return !(state.design_stage === "handoff_complete" && state.design_approved);
|
|
@@ -3615,18 +3015,18 @@ async function toolGuardHook(ctx, input, output) {
|
|
|
3615
3015
|
}
|
|
3616
3016
|
|
|
3617
3017
|
// src/hooks/session-start.ts
|
|
3618
|
-
import { existsSync as
|
|
3018
|
+
import { existsSync as existsSync22, readFileSync as readFileSync22 } from "fs";
|
|
3619
3019
|
async function sessionStartHook(ctx) {
|
|
3620
3020
|
const planningDir2 = ctx.directory + "/.planning";
|
|
3621
3021
|
const codebaseDirectory = codebaseDir(ctx.directory);
|
|
3622
3022
|
const workspaceRoot = findWorkspaceRoot(ctx.directory);
|
|
3623
3023
|
const config = workspaceRoot ? getWorkspaceConfig(ctx.directory) : null;
|
|
3624
|
-
if (!
|
|
3024
|
+
if (!existsSync22(planningDir2)) {
|
|
3625
3025
|
return {
|
|
3626
3026
|
flowdeck_phase: null,
|
|
3627
3027
|
flowdeck_status: "no_plan",
|
|
3628
3028
|
flowdeck_warning: "Run /fd-map-codebase to index the codebase, then /fd-new-feature to start a feature.",
|
|
3629
|
-
flowdeck_has_codebase:
|
|
3029
|
+
flowdeck_has_codebase: existsSync22(codebaseDirectory),
|
|
3630
3030
|
...workspaceRoot && config?.sub_repos ? {
|
|
3631
3031
|
flowdeck_workspace_root: workspaceRoot,
|
|
3632
3032
|
flowdeck_sub_repos: config.sub_repos,
|
|
@@ -3637,7 +3037,7 @@ async function sessionStartHook(ctx) {
|
|
|
3637
3037
|
}
|
|
3638
3038
|
try {
|
|
3639
3039
|
const stateFilePath = statePath(ctx.directory);
|
|
3640
|
-
const content =
|
|
3040
|
+
const content = readFileSync22(stateFilePath, "utf-8");
|
|
3641
3041
|
const state = parseState(content);
|
|
3642
3042
|
const currentPhase = state["current_phase"] || {};
|
|
3643
3043
|
const result = {
|
|
@@ -3645,7 +3045,7 @@ async function sessionStartHook(ctx) {
|
|
|
3645
3045
|
flowdeck_status: currentPhase["status"] ?? null,
|
|
3646
3046
|
flowdeck_steps_pending: currentPhase["steps_pending"] ?? null,
|
|
3647
3047
|
flowdeck_last_action: currentPhase["last_action"] ?? null,
|
|
3648
|
-
flowdeck_has_codebase:
|
|
3048
|
+
flowdeck_has_codebase: existsSync22(codebaseDirectory)
|
|
3649
3049
|
};
|
|
3650
3050
|
if (workspaceRoot && config?.sub_repos && config.sub_repos.length > 0) {
|
|
3651
3051
|
result.flowdeck_workspace_root = workspaceRoot;
|
|
@@ -3654,13 +3054,12 @@ async function sessionStartHook(ctx) {
|
|
|
3654
3054
|
result.flowdeck_is_workspace_root = ctx.directory === workspaceRoot;
|
|
3655
3055
|
}
|
|
3656
3056
|
return result;
|
|
3657
|
-
} catch
|
|
3658
|
-
console.warn("[flowdeck] Warning: State file unreadable. Continuing without flowdeck context.");
|
|
3057
|
+
} catch {
|
|
3659
3058
|
const result = {
|
|
3660
3059
|
flowdeck_phase: null,
|
|
3661
3060
|
flowdeck_status: "error",
|
|
3662
3061
|
flowdeck_warning: "State file unreadable. Continuing without flowdeck context.",
|
|
3663
|
-
flowdeck_has_codebase:
|
|
3062
|
+
flowdeck_has_codebase: existsSync22(codebaseDirectory)
|
|
3664
3063
|
};
|
|
3665
3064
|
if (workspaceRoot && config?.sub_repos && config.sub_repos.length > 0) {
|
|
3666
3065
|
result.flowdeck_workspace_root = workspaceRoot;
|
|
@@ -3690,7 +3089,7 @@ var COMPLETION_COMMANDS = new Set([
|
|
|
3690
3089
|
"execute",
|
|
3691
3090
|
"verify"
|
|
3692
3091
|
]);
|
|
3693
|
-
function
|
|
3092
|
+
function normalizeCommandName(raw) {
|
|
3694
3093
|
return raw.replace(/^\//, "").replace(/^fd-/, "");
|
|
3695
3094
|
}
|
|
3696
3095
|
function notify(title, body, level = "info") {
|
|
@@ -3699,9 +3098,7 @@ function notify(title, body, level = "info") {
|
|
|
3699
3098
|
if (platform === "linux") {
|
|
3700
3099
|
const urgency = level === "critical" ? "critical" : "normal";
|
|
3701
3100
|
const proc = execFile("notify-send", ["--urgency", urgency, "--app-name", "FlowDeck", "--icon", "dialog-information", title, body], { timeout: 3000 });
|
|
3702
|
-
proc.on("error", () => {
|
|
3703
|
-
tryTerminalBell();
|
|
3704
|
-
});
|
|
3101
|
+
proc.on("error", () => {});
|
|
3705
3102
|
} else if (platform === "darwin") {
|
|
3706
3103
|
const script = `display notification "${body.replace(/"/g, "\\\"")}" with title "${title.replace(/"/g, "\\\"")}" subtitle "FlowDeck"`;
|
|
3707
3104
|
const proc = execFile("osascript", ["-e", script], { timeout: 3000 });
|
|
@@ -3719,11 +3116,6 @@ function notify(title, body, level = "info") {
|
|
|
3719
3116
|
}
|
|
3720
3117
|
} catch {}
|
|
3721
3118
|
}
|
|
3722
|
-
function tryTerminalBell() {
|
|
3723
|
-
try {
|
|
3724
|
-
process.stdout.write("\x07");
|
|
3725
|
-
} catch {}
|
|
3726
|
-
}
|
|
3727
3119
|
|
|
3728
3120
|
class NotificationController {
|
|
3729
3121
|
pendingCommand = null;
|
|
@@ -3735,7 +3127,7 @@ class NotificationController {
|
|
|
3735
3127
|
this.log = log;
|
|
3736
3128
|
}
|
|
3737
3129
|
onCommandExecuted(rawCommand) {
|
|
3738
|
-
const name =
|
|
3130
|
+
const name = normalizeCommandName(rawCommand);
|
|
3739
3131
|
if (!INTERACTIVE_COMMANDS.has(name) && !COMPLETION_COMMANDS.has(name)) {
|
|
3740
3132
|
this.log(`[notify] command.executed: "${name}" — not a tracked command, skipping`);
|
|
3741
3133
|
return;
|
|
@@ -3799,13 +3191,13 @@ class NotificationController {
|
|
|
3799
3191
|
return this.lastNotifiedKey;
|
|
3800
3192
|
}
|
|
3801
3193
|
}
|
|
3802
|
-
function notifyPermissionNeeded(
|
|
3803
|
-
notify("FlowDeck Permission Required", `Agent needs approval to use tool: ${
|
|
3194
|
+
function notifyPermissionNeeded(tool15) {
|
|
3195
|
+
notify("FlowDeck Permission Required", `Agent needs approval to use tool: ${tool15}`, "critical");
|
|
3804
3196
|
}
|
|
3805
3197
|
|
|
3806
3198
|
// src/hooks/patch-trust.ts
|
|
3807
|
-
import { existsSync as
|
|
3808
|
-
import { join as
|
|
3199
|
+
import { existsSync as existsSync23, readFileSync as readFileSync23 } from "fs";
|
|
3200
|
+
import { join as join21 } from "path";
|
|
3809
3201
|
var HIGH_RISK_KEYWORDS = [
|
|
3810
3202
|
"password",
|
|
3811
3203
|
"secret",
|
|
@@ -3826,26 +3218,12 @@ var HIGH_RISK_KEYWORDS = [
|
|
|
3826
3218
|
"root",
|
|
3827
3219
|
"privilege"
|
|
3828
3220
|
];
|
|
3829
|
-
function loadVolatility(directory) {
|
|
3830
|
-
const p = join25(codebaseDir(directory), "VOLATILITY.json");
|
|
3831
|
-
if (!existsSync27(p))
|
|
3832
|
-
return {};
|
|
3833
|
-
try {
|
|
3834
|
-
const data = JSON.parse(readFileSync26(p, "utf-8"));
|
|
3835
|
-
const map = {};
|
|
3836
|
-
for (const entry of data.entries ?? [])
|
|
3837
|
-
map[entry.path] = entry.stability;
|
|
3838
|
-
return map;
|
|
3839
|
-
} catch {
|
|
3840
|
-
return {};
|
|
3841
|
-
}
|
|
3842
|
-
}
|
|
3843
3221
|
function loadFailedPaths(directory) {
|
|
3844
|
-
const p =
|
|
3845
|
-
if (!
|
|
3222
|
+
const p = join21(codebaseDir(directory), "FAILURES.json");
|
|
3223
|
+
if (!existsSync23(p))
|
|
3846
3224
|
return [];
|
|
3847
3225
|
try {
|
|
3848
|
-
const data = JSON.parse(
|
|
3226
|
+
const data = JSON.parse(readFileSync23(p, "utf-8"));
|
|
3849
3227
|
return (data.entries ?? []).flatMap((e) => e.affected_paths ?? []);
|
|
3850
3228
|
} catch {
|
|
3851
3229
|
return [];
|
|
@@ -3854,18 +3232,6 @@ function loadFailedPaths(directory) {
|
|
|
3854
3232
|
function scorePatch(directory, filePath, content) {
|
|
3855
3233
|
let score = 100;
|
|
3856
3234
|
const signals = [];
|
|
3857
|
-
const volatility = loadVolatility(directory);
|
|
3858
|
-
const stability = Object.entries(volatility).find(([path]) => filePath.includes(path))?.[1];
|
|
3859
|
-
if (stability === "critical") {
|
|
3860
|
-
score -= 40;
|
|
3861
|
-
signals.push("file is in critical volatility zone");
|
|
3862
|
-
} else if (stability === "volatile") {
|
|
3863
|
-
score -= 25;
|
|
3864
|
-
signals.push("file is in volatile zone");
|
|
3865
|
-
} else if (stability === "moderate") {
|
|
3866
|
-
score -= 10;
|
|
3867
|
-
signals.push("file has moderate churn");
|
|
3868
|
-
}
|
|
3869
3235
|
const failedPaths = loadFailedPaths(directory);
|
|
3870
3236
|
if (failedPaths.some((p) => filePath.includes(p))) {
|
|
3871
3237
|
score -= 20;
|
|
@@ -3910,8 +3276,8 @@ async function patchTrustHook(ctx, input, output) {
|
|
|
3910
3276
|
}
|
|
3911
3277
|
|
|
3912
3278
|
// src/hooks/decision-trace-hook.ts
|
|
3913
|
-
import { existsSync as
|
|
3914
|
-
import { join as
|
|
3279
|
+
import { existsSync as existsSync24, mkdirSync as mkdirSync12, appendFileSync as appendFileSync4 } from "fs";
|
|
3280
|
+
import { join as join22 } from "path";
|
|
3915
3281
|
async function decisionTraceHook(ctx, input, output) {
|
|
3916
3282
|
if (input.tool !== "write" && input.tool !== "edit")
|
|
3917
3283
|
return;
|
|
@@ -3920,8 +3286,8 @@ async function decisionTraceHook(ctx, input, output) {
|
|
|
3920
3286
|
return;
|
|
3921
3287
|
const base = codebaseDir(ctx.directory);
|
|
3922
3288
|
try {
|
|
3923
|
-
if (!
|
|
3924
|
-
|
|
3289
|
+
if (!existsSync24(base))
|
|
3290
|
+
mkdirSync12(base, { recursive: true });
|
|
3925
3291
|
const entry = {
|
|
3926
3292
|
timestamp: new Date().toISOString(),
|
|
3927
3293
|
file_path: filePath,
|
|
@@ -3933,164 +3299,14 @@ async function decisionTraceHook(ctx, input, output) {
|
|
|
3933
3299
|
risk_level: "unknown",
|
|
3934
3300
|
auto_recorded: true
|
|
3935
3301
|
};
|
|
3936
|
-
appendFileSync4(
|
|
3302
|
+
appendFileSync4(join22(base, "DECISIONS.jsonl"), JSON.stringify(entry) + `
|
|
3937
3303
|
`, "utf-8");
|
|
3938
3304
|
} catch {}
|
|
3939
3305
|
}
|
|
3940
3306
|
|
|
3941
|
-
// src/services/telemetry.ts
|
|
3942
|
-
import { existsSync as existsSync29, readFileSync as readFileSync27, appendFileSync as appendFileSync5, mkdirSync as mkdirSync15 } from "fs";
|
|
3943
|
-
import { join as join27 } from "path";
|
|
3944
|
-
import { randomUUID } from "crypto";
|
|
3945
|
-
function telemetryPath(dir) {
|
|
3946
|
-
return join27(codebaseDir(dir), "TELEMETRY.jsonl");
|
|
3947
|
-
}
|
|
3948
|
-
function appendEvent2(dir, partial) {
|
|
3949
|
-
if (process.env.TELEMETRY_ENABLED !== "true")
|
|
3950
|
-
return null;
|
|
3951
|
-
const cd = codebaseDir(dir);
|
|
3952
|
-
if (!existsSync29(cd))
|
|
3953
|
-
mkdirSync15(cd, { recursive: true });
|
|
3954
|
-
const event = {
|
|
3955
|
-
id: randomUUID(),
|
|
3956
|
-
ts: new Date().toISOString(),
|
|
3957
|
-
...partial
|
|
3958
|
-
};
|
|
3959
|
-
appendFileSync5(telemetryPath(dir), JSON.stringify(event) + `
|
|
3960
|
-
`, "utf-8");
|
|
3961
|
-
return event;
|
|
3962
|
-
}
|
|
3963
|
-
|
|
3964
|
-
// src/hooks/telemetry-hook.ts
|
|
3965
|
-
var toolStartTimes = new Map;
|
|
3966
|
-
var REPORTABLE_TOOLS = new Set([
|
|
3967
|
-
"delegate",
|
|
3968
|
-
"run-pipeline",
|
|
3969
|
-
"council",
|
|
3970
|
-
"bash",
|
|
3971
|
-
"write",
|
|
3972
|
-
"edit",
|
|
3973
|
-
"read",
|
|
3974
|
-
"codegraph",
|
|
3975
|
-
"codebase-state",
|
|
3976
|
-
"planning-state",
|
|
3977
|
-
"workspace-state",
|
|
3978
|
-
"repo-memory",
|
|
3979
|
-
"hash-edit",
|
|
3980
|
-
"context-generator",
|
|
3981
|
-
"volatility-map",
|
|
3982
|
-
"failure-replay",
|
|
3983
|
-
"decision-trace",
|
|
3984
|
-
"policy-engine",
|
|
3985
|
-
"reflect"
|
|
3986
|
-
]);
|
|
3987
|
-
var SELF_LOGGING_TOOLS = new Set(["delegate", "run-pipeline", "council"]);
|
|
3988
|
-
function correlationKey(sessionId, runId, tool19) {
|
|
3989
|
-
return `${sessionId}:${runId}:${tool19}`;
|
|
3990
|
-
}
|
|
3991
|
-
function resolveIds(toolInput) {
|
|
3992
|
-
const session_id = toolInput.sessionID ?? toolInput.sessionId ?? process.env.OPENCODE_SESSION_ID ?? "session-0";
|
|
3993
|
-
const run_id = toolInput.messageID ?? toolInput.messageId ?? toolInput.runID ?? toolInput.runId ?? process.env.OPENCODE_RUN_ID ?? "run-0";
|
|
3994
|
-
return { session_id, run_id };
|
|
3995
|
-
}
|
|
3996
|
-
function inferStatus(output) {
|
|
3997
|
-
if (output.error)
|
|
3998
|
-
return "error";
|
|
3999
|
-
if (typeof output.output !== "string")
|
|
4000
|
-
return "ok";
|
|
4001
|
-
const text = output.output.trim();
|
|
4002
|
-
if (!text)
|
|
4003
|
-
return "ok";
|
|
4004
|
-
try {
|
|
4005
|
-
const parsed = JSON.parse(text);
|
|
4006
|
-
if (parsed.success === false || parsed.error || parsed.status === "error")
|
|
4007
|
-
return "error";
|
|
4008
|
-
return "ok";
|
|
4009
|
-
} catch {
|
|
4010
|
-
return "ok";
|
|
4011
|
-
}
|
|
4012
|
-
}
|
|
4013
|
-
function buildInputSummary(tool19, args) {
|
|
4014
|
-
if (tool19 === "delegate" || tool19 === "run-pipeline" || tool19 === "council") {
|
|
4015
|
-
return "";
|
|
4016
|
-
}
|
|
4017
|
-
if (tool19 === "bash" || tool19 === "write" || tool19 === "edit" || tool19 === "read") {
|
|
4018
|
-
const path = args.path ?? args.filePath ?? args.file ?? "";
|
|
4019
|
-
const cmd = args.command ?? args.cmd ?? "";
|
|
4020
|
-
return path || cmd ? summarize(String(path || cmd), 80) : "";
|
|
4021
|
-
}
|
|
4022
|
-
const firstStr = Object.values(args).find((v) => typeof v === "string");
|
|
4023
|
-
return firstStr ? summarize(firstStr, 80) : "";
|
|
4024
|
-
}
|
|
4025
|
-
async function telemetryHook(context, toolInput, output, reporter) {
|
|
4026
|
-
const dir = context.directory ?? process.cwd();
|
|
4027
|
-
const tool19 = toolInput.name ?? toolInput.tool ?? "unknown";
|
|
4028
|
-
const ids = resolveIds(toolInput);
|
|
4029
|
-
const key = correlationKey(ids.session_id, ids.run_id, tool19);
|
|
4030
|
-
toolStartTimes.set(key, Date.now());
|
|
4031
|
-
appendEvent2(dir, {
|
|
4032
|
-
session_id: ids.session_id,
|
|
4033
|
-
run_id: ids.run_id,
|
|
4034
|
-
event: "tool.call",
|
|
4035
|
-
tool: tool19,
|
|
4036
|
-
status: "ok",
|
|
4037
|
-
meta: { parameters: output.args ?? {} }
|
|
4038
|
-
});
|
|
4039
|
-
if (reporter && REPORTABLE_TOOLS.has(tool19) && !SELF_LOGGING_TOOLS.has(tool19)) {
|
|
4040
|
-
const inputSummary = buildInputSummary(tool19, output.args ?? {});
|
|
4041
|
-
reporter.reportToolStarted(tool19, inputSummary, {
|
|
4042
|
-
session_id: ids.session_id,
|
|
4043
|
-
run_id: ids.run_id
|
|
4044
|
-
});
|
|
4045
|
-
}
|
|
4046
|
-
if (reporter && REPORTABLE_TOOLS.has(tool19)) {
|
|
4047
|
-
reporter.trackStart(key);
|
|
4048
|
-
}
|
|
4049
|
-
}
|
|
4050
|
-
async function telemetryAfterHook(context, toolInput, output, reporter) {
|
|
4051
|
-
const dir = context.directory ?? process.cwd();
|
|
4052
|
-
const tool19 = toolInput.name ?? toolInput.tool ?? "unknown";
|
|
4053
|
-
const ids = resolveIds(toolInput);
|
|
4054
|
-
const key = correlationKey(ids.session_id, ids.run_id, tool19);
|
|
4055
|
-
const status = inferStatus(output);
|
|
4056
|
-
let duration_ms;
|
|
4057
|
-
if (reporter && REPORTABLE_TOOLS.has(tool19)) {
|
|
4058
|
-
duration_ms = reporter.elapsedMs(key);
|
|
4059
|
-
}
|
|
4060
|
-
if (duration_ms === undefined) {
|
|
4061
|
-
const startMs = toolStartTimes.get(key);
|
|
4062
|
-
if (startMs !== undefined)
|
|
4063
|
-
duration_ms = Date.now() - startMs;
|
|
4064
|
-
}
|
|
4065
|
-
toolStartTimes.delete(key);
|
|
4066
|
-
let result_summary;
|
|
4067
|
-
if (typeof output.output === "string") {
|
|
4068
|
-
result_summary = summarize(output.output, 100);
|
|
4069
|
-
} else if (output.error) {
|
|
4070
|
-
result_summary = summarize(String(output.error), 100);
|
|
4071
|
-
}
|
|
4072
|
-
appendEvent2(dir, {
|
|
4073
|
-
session_id: ids.session_id,
|
|
4074
|
-
run_id: ids.run_id,
|
|
4075
|
-
event: status === "error" ? "tool.failed" : "tool.complete",
|
|
4076
|
-
tool: tool19,
|
|
4077
|
-
status,
|
|
4078
|
-
duration_ms,
|
|
4079
|
-
result_summary
|
|
4080
|
-
});
|
|
4081
|
-
if (reporter && REPORTABLE_TOOLS.has(tool19) && !SELF_LOGGING_TOOLS.has(tool19)) {
|
|
4082
|
-
if (status === "error") {
|
|
4083
|
-
const errText = output.error ? String(output.error) : typeof output.output === "string" ? output.output : "unknown error";
|
|
4084
|
-
reporter.reportToolFailed(tool19, duration_ms, errText);
|
|
4085
|
-
} else {
|
|
4086
|
-
reporter.reportToolCompleted(tool19, duration_ms, result_summary ?? "");
|
|
4087
|
-
}
|
|
4088
|
-
}
|
|
4089
|
-
}
|
|
4090
|
-
|
|
4091
3307
|
// src/services/approval-manager.ts
|
|
4092
|
-
import { existsSync as
|
|
4093
|
-
import { join as
|
|
3308
|
+
import { existsSync as existsSync25, readFileSync as readFileSync24, writeFileSync as writeFileSync13, mkdirSync as mkdirSync13 } from "fs";
|
|
3309
|
+
import { join as join23 } from "path";
|
|
4094
3310
|
var APPROVAL_TTL_MS = 30 * 60 * 1000;
|
|
4095
3311
|
var SENSITIVE_PATTERNS = [
|
|
4096
3312
|
/auth/i,
|
|
@@ -4127,14 +3343,14 @@ function isSensitivePath(filePath) {
|
|
|
4127
3343
|
return SENSITIVE_PATTERNS.some((p) => p.test(filePath));
|
|
4128
3344
|
}
|
|
4129
3345
|
function approvalsPath(dir) {
|
|
4130
|
-
return
|
|
3346
|
+
return join23(codebaseDir(dir), "APPROVALS.json");
|
|
4131
3347
|
}
|
|
4132
3348
|
function loadStore2(dir) {
|
|
4133
3349
|
const p = approvalsPath(dir);
|
|
4134
|
-
if (!
|
|
3350
|
+
if (!existsSync25(p))
|
|
4135
3351
|
return { requests: [] };
|
|
4136
3352
|
try {
|
|
4137
|
-
return JSON.parse(
|
|
3353
|
+
return JSON.parse(readFileSync24(p, "utf-8"));
|
|
4138
3354
|
} catch {
|
|
4139
3355
|
return { requests: [] };
|
|
4140
3356
|
}
|
|
@@ -4152,8 +3368,8 @@ async function approvalHook(context, toolInput, output) {
|
|
|
4152
3368
|
if (!ENABLED2)
|
|
4153
3369
|
return;
|
|
4154
3370
|
const dir = context.directory ?? process.cwd();
|
|
4155
|
-
const
|
|
4156
|
-
if (!WRITE_TOOLS.has(
|
|
3371
|
+
const tool15 = toolInput.name ?? toolInput.tool ?? "";
|
|
3372
|
+
if (!WRITE_TOOLS.has(tool15))
|
|
4157
3373
|
return;
|
|
4158
3374
|
const args = output.args ?? {};
|
|
4159
3375
|
const filePath = String(args.path ?? args.file_path ?? args.filename ?? "");
|
|
@@ -4164,20 +3380,306 @@ async function approvalHook(context, toolInput, output) {
|
|
|
4164
3380
|
const approval = checkApproval(dir, filePath, "");
|
|
4165
3381
|
if (approval)
|
|
4166
3382
|
return;
|
|
4167
|
-
appendEvent2(dir, {
|
|
4168
|
-
session_id: process.env.OPENCODE_SESSION_ID ?? "session-0",
|
|
4169
|
-
run_id: process.env.OPENCODE_RUN_ID ?? "run-0",
|
|
4170
|
-
event: "approval.request",
|
|
4171
|
-
tool: tool19,
|
|
4172
|
-
status: "blocked",
|
|
4173
|
-
files: [filePath],
|
|
4174
|
-
meta: { trigger: "sensitive_file", file: filePath }
|
|
4175
|
-
});
|
|
4176
3383
|
throw new Error(`APPROVAL_REQUIRED: "${filePath}" is a sensitive file (auth/payment/secrets/infra).
|
|
4177
3384
|
` + `Risk level: HIGH — manual approval needed before editing.
|
|
4178
3385
|
` + `To proceed: run /fd-guarded-edit --file "${filePath}" to review and approve this change.`);
|
|
4179
3386
|
}
|
|
4180
3387
|
|
|
3388
|
+
// src/services/event-logger.ts
|
|
3389
|
+
import { existsSync as existsSync26, mkdirSync as mkdirSync14, appendFileSync as appendFileSync5, readFileSync as readFileSync25, writeFileSync as writeFileSync14, renameSync, unlinkSync, statSync as statSync2 } from "fs";
|
|
3390
|
+
import { join as join24, resolve as resolve2, sep } from "path";
|
|
3391
|
+
var SENSITIVE_KEYS = [
|
|
3392
|
+
"password",
|
|
3393
|
+
"token",
|
|
3394
|
+
"apikey",
|
|
3395
|
+
"api_key",
|
|
3396
|
+
"secret",
|
|
3397
|
+
"authorization",
|
|
3398
|
+
"auth",
|
|
3399
|
+
"key",
|
|
3400
|
+
"credential",
|
|
3401
|
+
"privatekey",
|
|
3402
|
+
"private_key",
|
|
3403
|
+
"accesstoken",
|
|
3404
|
+
"access_token",
|
|
3405
|
+
"refreshtoken",
|
|
3406
|
+
"refresh_token"
|
|
3407
|
+
];
|
|
3408
|
+
var currentAgent = null;
|
|
3409
|
+
function getCurrentAgent() {
|
|
3410
|
+
return currentAgent;
|
|
3411
|
+
}
|
|
3412
|
+
function setCurrentAgent(agent) {
|
|
3413
|
+
currentAgent = agent;
|
|
3414
|
+
}
|
|
3415
|
+
function sanitizeArgs(args) {
|
|
3416
|
+
if (!args || typeof args !== "object")
|
|
3417
|
+
return {};
|
|
3418
|
+
const result = {};
|
|
3419
|
+
for (const [key, value] of Object.entries(args)) {
|
|
3420
|
+
const lowerKey = key.toLowerCase();
|
|
3421
|
+
if (SENSITIVE_KEYS.some((sk) => lowerKey.includes(sk))) {
|
|
3422
|
+
result[key] = "[REDACTED]";
|
|
3423
|
+
} else if (key === "content" || key === "newString" || key === "oldString" || key === "template") {
|
|
3424
|
+
if (typeof value === "string" && value.length > 100) {
|
|
3425
|
+
result[key] = `[${value.length} chars truncated]`;
|
|
3426
|
+
} else {
|
|
3427
|
+
result[key] = value;
|
|
3428
|
+
}
|
|
3429
|
+
} else {
|
|
3430
|
+
result[key] = value;
|
|
3431
|
+
}
|
|
3432
|
+
}
|
|
3433
|
+
return result;
|
|
3434
|
+
}
|
|
3435
|
+
function isValidDirectory(directory) {
|
|
3436
|
+
const normalized = resolve2(directory);
|
|
3437
|
+
if (normalized !== directory && !directory.startsWith(sep)) {
|
|
3438
|
+
return false;
|
|
3439
|
+
}
|
|
3440
|
+
if (directory.includes("..") || directory.includes(".." + sep)) {
|
|
3441
|
+
return false;
|
|
3442
|
+
}
|
|
3443
|
+
try {
|
|
3444
|
+
const stats = statSync2(directory);
|
|
3445
|
+
return stats.isDirectory();
|
|
3446
|
+
} catch {
|
|
3447
|
+
return false;
|
|
3448
|
+
}
|
|
3449
|
+
}
|
|
3450
|
+
function logEvent(directory, event, log) {
|
|
3451
|
+
if (process.env.FLOWDECK_EVENT_LOG === "off")
|
|
3452
|
+
return;
|
|
3453
|
+
if (!isValidDirectory(directory))
|
|
3454
|
+
return;
|
|
3455
|
+
const logDir = join24(directory, ".opencode");
|
|
3456
|
+
const logPath = join24(logDir, "flowdeck-events.jsonl");
|
|
3457
|
+
try {
|
|
3458
|
+
if (!existsSync26(logDir)) {
|
|
3459
|
+
mkdirSync14(logDir, { recursive: true });
|
|
3460
|
+
}
|
|
3461
|
+
appendFileSync5(logPath, JSON.stringify(event) + `
|
|
3462
|
+
`, "utf-8");
|
|
3463
|
+
rotateLogFile(logPath);
|
|
3464
|
+
if (log) {
|
|
3465
|
+
log(formatEventForStderr(event));
|
|
3466
|
+
}
|
|
3467
|
+
} catch {}
|
|
3468
|
+
}
|
|
3469
|
+
function rotateLogFile(logPath) {
|
|
3470
|
+
try {
|
|
3471
|
+
const stats = statSync2(logPath);
|
|
3472
|
+
if (stats.size < 5000)
|
|
3473
|
+
return;
|
|
3474
|
+
const content = readFileSync25(logPath, "utf-8");
|
|
3475
|
+
const lines = content.split(`
|
|
3476
|
+
`).filter((l) => l.trim());
|
|
3477
|
+
if (lines.length > 1000) {
|
|
3478
|
+
const backupPath = logPath + ".backup";
|
|
3479
|
+
renameSync(logPath, backupPath);
|
|
3480
|
+
const keep = lines.slice(-1000);
|
|
3481
|
+
writeFileSync14(logPath, keep.join(`
|
|
3482
|
+
`) + `
|
|
3483
|
+
`, "utf-8");
|
|
3484
|
+
try {
|
|
3485
|
+
unlinkSync(backupPath);
|
|
3486
|
+
} catch {}
|
|
3487
|
+
}
|
|
3488
|
+
} catch {}
|
|
3489
|
+
}
|
|
3490
|
+
function formatEventForStderr(event) {
|
|
3491
|
+
const time = event.timestamp.slice(11, 23);
|
|
3492
|
+
const agent = event.agent ?? "unknown";
|
|
3493
|
+
const dim = "\x1B[2m";
|
|
3494
|
+
const reset = "\x1B[0m";
|
|
3495
|
+
const cyan = "\x1B[36m";
|
|
3496
|
+
switch (event.type) {
|
|
3497
|
+
case "tool.before": {
|
|
3498
|
+
let icon;
|
|
3499
|
+
if (event.tool === "write" || event.tool === "edit")
|
|
3500
|
+
icon = "✏️ ";
|
|
3501
|
+
else if (event.tool === "read")
|
|
3502
|
+
icon = "\uD83D\uDD0D";
|
|
3503
|
+
else if (event.tool === "bash" || event.tool === "shell")
|
|
3504
|
+
icon = "\uD83C\uDFC3";
|
|
3505
|
+
else if (event.tool === "delegate")
|
|
3506
|
+
icon = "\uD83E\uDD16";
|
|
3507
|
+
else
|
|
3508
|
+
icon = "\uD83D\uDD27";
|
|
3509
|
+
const argStr = formatArgs(event.args);
|
|
3510
|
+
const thinking = event.thinking ? ` "${event.thinking}"` : "";
|
|
3511
|
+
return `${dim}[${time}]${reset} ${icon} ${cyan}${agent}${reset} → ${event.tool}(${argStr})${thinking}`;
|
|
3512
|
+
}
|
|
3513
|
+
case "tool.after": {
|
|
3514
|
+
let icon;
|
|
3515
|
+
let statusColor;
|
|
3516
|
+
if (event.status === "success") {
|
|
3517
|
+
icon = "✅";
|
|
3518
|
+
statusColor = "\x1B[32m";
|
|
3519
|
+
} else if (event.status === "error") {
|
|
3520
|
+
icon = "❌";
|
|
3521
|
+
statusColor = "\x1B[31m";
|
|
3522
|
+
} else if (event.status === "blocked") {
|
|
3523
|
+
icon = "⛔";
|
|
3524
|
+
statusColor = "\x1B[33m";
|
|
3525
|
+
} else {
|
|
3526
|
+
icon = "✅";
|
|
3527
|
+
statusColor = "\x1B[32m";
|
|
3528
|
+
}
|
|
3529
|
+
const argStr = formatArgs(event.args);
|
|
3530
|
+
const duration = event.duration_ms ? ` done in ${event.duration_ms}ms` : "";
|
|
3531
|
+
const error = event.error ? ` error: ${event.error}` : "";
|
|
3532
|
+
return `${dim}[${time}]${reset} ${icon} ${cyan}${agent}${reset} → ${event.tool}(${argStr})${statusColor}${duration}${error}${reset}`;
|
|
3533
|
+
}
|
|
3534
|
+
case "agent.delegated": {
|
|
3535
|
+
const thinking = event.thinking ? ` "${event.thinking}"` : "";
|
|
3536
|
+
return `${dim}[${time}]${reset} \uD83E\uDD16 ${cyan}${agent}${reset} → delegate(${thinking})`;
|
|
3537
|
+
}
|
|
3538
|
+
case "session.created":
|
|
3539
|
+
return `${dim}[${time}]${reset} \uD83D\uDCC2 session created${event.session_id ? ` (${event.session_id})` : ""}`;
|
|
3540
|
+
case "session.idle":
|
|
3541
|
+
return `${dim}[${time}]${reset} \uD83D\uDCA4 session idle${event.session_id ? ` (${event.session_id})` : ""}`;
|
|
3542
|
+
case "session.error":
|
|
3543
|
+
return `${dim}[${time}]${reset} ❌ session error${event.error ? `: ${event.error}` : ""}`;
|
|
3544
|
+
default:
|
|
3545
|
+
return `${dim}[${time}]${reset} ${event.type}`;
|
|
3546
|
+
}
|
|
3547
|
+
}
|
|
3548
|
+
function formatArgs(args) {
|
|
3549
|
+
if (!args)
|
|
3550
|
+
return "";
|
|
3551
|
+
const parts = [];
|
|
3552
|
+
for (const [key, value] of Object.entries(args)) {
|
|
3553
|
+
if (key === "filePath" || key === "path" || key === "file") {
|
|
3554
|
+
parts.push(String(value));
|
|
3555
|
+
} else if (key === "agent") {
|
|
3556
|
+
parts.push(`@${String(value)}`);
|
|
3557
|
+
}
|
|
3558
|
+
}
|
|
3559
|
+
return parts.join(", ");
|
|
3560
|
+
}
|
|
3561
|
+
|
|
3562
|
+
// src/hooks/event-log-hook.ts
|
|
3563
|
+
var toolStartTimes = new Map;
|
|
3564
|
+
var staleThresholdMs = 5 * 60 * 1000;
|
|
3565
|
+
var CLEANUP_INTERVAL = 50;
|
|
3566
|
+
var beforeHookCallCount = 0;
|
|
3567
|
+
function cleanupStaleToolStartTimes() {
|
|
3568
|
+
const now = Date.now();
|
|
3569
|
+
for (const [key, startTime] of toolStartTimes.entries()) {
|
|
3570
|
+
if (now - startTime > staleThresholdMs) {
|
|
3571
|
+
toolStartTimes.delete(key);
|
|
3572
|
+
}
|
|
3573
|
+
}
|
|
3574
|
+
}
|
|
3575
|
+
function createEventLogHooks(appLog) {
|
|
3576
|
+
return {
|
|
3577
|
+
async before(ctx, toolInput, toolOutput) {
|
|
3578
|
+
const toolName = toolInput.tool ?? toolInput.name ?? "unknown";
|
|
3579
|
+
const sessionId = toolInput.sessionID ?? toolInput.sessionId ?? "unknown";
|
|
3580
|
+
const args = toolOutput?.args ?? toolInput?.args ?? {};
|
|
3581
|
+
const startKey = `${sessionId}:${toolName}`;
|
|
3582
|
+
beforeHookCallCount++;
|
|
3583
|
+
if (beforeHookCallCount >= CLEANUP_INTERVAL) {
|
|
3584
|
+
beforeHookCallCount = 0;
|
|
3585
|
+
cleanupStaleToolStartTimes();
|
|
3586
|
+
}
|
|
3587
|
+
toolStartTimes.set(startKey, Date.now());
|
|
3588
|
+
const event = {
|
|
3589
|
+
timestamp: new Date().toISOString(),
|
|
3590
|
+
type: "tool.before",
|
|
3591
|
+
agent: getCurrentAgent() ?? undefined,
|
|
3592
|
+
tool: toolName,
|
|
3593
|
+
args: sanitizeArgs(args),
|
|
3594
|
+
session_id: sessionId
|
|
3595
|
+
};
|
|
3596
|
+
logEvent(ctx.directory, event, appLog);
|
|
3597
|
+
},
|
|
3598
|
+
async after(ctx, toolInput, toolOutput) {
|
|
3599
|
+
const toolName = toolInput.tool ?? toolInput.name ?? "unknown";
|
|
3600
|
+
const sessionId = toolInput.sessionID ?? toolInput.sessionId ?? "unknown";
|
|
3601
|
+
const args = toolOutput?.args ?? toolInput?.args ?? {};
|
|
3602
|
+
const startKey = `${sessionId}:${toolName}`;
|
|
3603
|
+
const startTime = toolStartTimes.get(startKey);
|
|
3604
|
+
const durationMs = startTime ? Date.now() - startTime : undefined;
|
|
3605
|
+
toolStartTimes.delete(startKey);
|
|
3606
|
+
let status = "success";
|
|
3607
|
+
let error;
|
|
3608
|
+
if (toolOutput?.error != null) {
|
|
3609
|
+
status = "error";
|
|
3610
|
+
error = typeof toolOutput.error === "string" ? toolOutput.error : String(toolOutput.error);
|
|
3611
|
+
} else if (toolOutput?.status === "error") {
|
|
3612
|
+
status = "error";
|
|
3613
|
+
error = typeof toolOutput.error === "string" ? toolOutput.error : "Unknown error";
|
|
3614
|
+
} else if (toolOutput?.status === "blocked") {
|
|
3615
|
+
status = "blocked";
|
|
3616
|
+
}
|
|
3617
|
+
const event = {
|
|
3618
|
+
timestamp: new Date().toISOString(),
|
|
3619
|
+
type: "tool.after",
|
|
3620
|
+
agent: getCurrentAgent() ?? undefined,
|
|
3621
|
+
tool: toolName,
|
|
3622
|
+
args: sanitizeArgs(args),
|
|
3623
|
+
duration_ms: durationMs,
|
|
3624
|
+
status,
|
|
3625
|
+
error,
|
|
3626
|
+
session_id: sessionId
|
|
3627
|
+
};
|
|
3628
|
+
logEvent(ctx.directory, event, appLog);
|
|
3629
|
+
},
|
|
3630
|
+
async session(ctx, event) {
|
|
3631
|
+
const type = event?.type ?? "";
|
|
3632
|
+
const props = event?.properties ?? {};
|
|
3633
|
+
if (type === "session.created") {
|
|
3634
|
+
if (props.parentID) {
|
|
3635
|
+
const agentName = extractAgentFromEvent(props);
|
|
3636
|
+
setCurrentAgent(agentName);
|
|
3637
|
+
}
|
|
3638
|
+
const toolEvent = {
|
|
3639
|
+
timestamp: new Date().toISOString(),
|
|
3640
|
+
type: "session.created",
|
|
3641
|
+
session_id: props.id ?? props.sessionId ?? undefined
|
|
3642
|
+
};
|
|
3643
|
+
logEvent(ctx.directory, toolEvent, appLog);
|
|
3644
|
+
} else if (type === "session.idle") {
|
|
3645
|
+
if (props.parentID) {
|
|
3646
|
+
setCurrentAgent(null);
|
|
3647
|
+
}
|
|
3648
|
+
const toolEvent = {
|
|
3649
|
+
timestamp: new Date().toISOString(),
|
|
3650
|
+
type: "session.idle",
|
|
3651
|
+
session_id: props.id ?? props.sessionId ?? undefined
|
|
3652
|
+
};
|
|
3653
|
+
logEvent(ctx.directory, toolEvent, appLog);
|
|
3654
|
+
} else if (type === "session.error") {
|
|
3655
|
+
if (props.parentID) {
|
|
3656
|
+
setCurrentAgent(null);
|
|
3657
|
+
}
|
|
3658
|
+
const err = props.error;
|
|
3659
|
+
const errorMsg = (err && typeof err === "object" && "message" in err ? String(err.message) : undefined) ?? (typeof err === "string" ? err : undefined) ?? undefined;
|
|
3660
|
+
const toolEvent = {
|
|
3661
|
+
timestamp: new Date().toISOString(),
|
|
3662
|
+
type: "session.error",
|
|
3663
|
+
session_id: props.id ?? props.sessionId ?? undefined,
|
|
3664
|
+
error: errorMsg
|
|
3665
|
+
};
|
|
3666
|
+
logEvent(ctx.directory, toolEvent, appLog);
|
|
3667
|
+
}
|
|
3668
|
+
}
|
|
3669
|
+
};
|
|
3670
|
+
}
|
|
3671
|
+
function extractAgentFromEvent(props) {
|
|
3672
|
+
if (typeof props.agent === "string")
|
|
3673
|
+
return props.agent;
|
|
3674
|
+
if (typeof props.name === "string")
|
|
3675
|
+
return props.name;
|
|
3676
|
+
const title = typeof props.title === "string" ? props.title : "";
|
|
3677
|
+
const match = title.match(/^(.+)-delegate$/);
|
|
3678
|
+
if (match)
|
|
3679
|
+
return match[1];
|
|
3680
|
+
return "unknown";
|
|
3681
|
+
}
|
|
3682
|
+
|
|
4181
3683
|
// src/hooks/context-window-monitor.ts
|
|
4182
3684
|
var CONTEXT_WARNING_THRESHOLD = 0.7;
|
|
4183
3685
|
var DEFAULT_CONTEXT_LIMIT = Number(process.env.FLOWDECK_CONTEXT_LIMIT) || 200000;
|
|
@@ -4229,8 +3731,8 @@ function createContextWindowMonitorHook() {
|
|
|
4229
3731
|
}
|
|
4230
3732
|
|
|
4231
3733
|
// src/hooks/shell-env-hook.ts
|
|
4232
|
-
import { existsSync as
|
|
4233
|
-
import { join as
|
|
3734
|
+
import { existsSync as existsSync27, readFileSync as readFileSync26 } from "fs";
|
|
3735
|
+
import { join as join25 } from "path";
|
|
4234
3736
|
import { createRequire as createRequire2 } from "module";
|
|
4235
3737
|
var _version;
|
|
4236
3738
|
function getVersion() {
|
|
@@ -4266,7 +3768,7 @@ var MARKER_TO_LANG = {
|
|
|
4266
3768
|
};
|
|
4267
3769
|
function detectPackageManager(root) {
|
|
4268
3770
|
for (const [lockfile, pm] of Object.entries(LOCKFILE_TO_PM)) {
|
|
4269
|
-
if (
|
|
3771
|
+
if (existsSync27(join25(root, lockfile)))
|
|
4270
3772
|
return pm;
|
|
4271
3773
|
}
|
|
4272
3774
|
return;
|
|
@@ -4275,7 +3777,7 @@ function detectLanguages(root) {
|
|
|
4275
3777
|
const langs = [];
|
|
4276
3778
|
const seen = new Set;
|
|
4277
3779
|
for (const [marker, lang] of Object.entries(MARKER_TO_LANG)) {
|
|
4278
|
-
if (!seen.has(lang) &&
|
|
3780
|
+
if (!seen.has(lang) && existsSync27(join25(root, marker))) {
|
|
4279
3781
|
langs.push(lang);
|
|
4280
3782
|
seen.add(lang);
|
|
4281
3783
|
}
|
|
@@ -4283,11 +3785,11 @@ function detectLanguages(root) {
|
|
|
4283
3785
|
return langs;
|
|
4284
3786
|
}
|
|
4285
3787
|
function readCurrentPhase(root) {
|
|
4286
|
-
const statePath2 =
|
|
4287
|
-
if (!
|
|
3788
|
+
const statePath2 = join25(root, ".planning", "STATE.md");
|
|
3789
|
+
if (!existsSync27(statePath2))
|
|
4288
3790
|
return;
|
|
4289
3791
|
try {
|
|
4290
|
-
const content =
|
|
3792
|
+
const content = readFileSync26(statePath2, "utf-8");
|
|
4291
3793
|
const match = content.match(/phase:\s*(\S+)/i);
|
|
4292
3794
|
return match?.[1];
|
|
4293
3795
|
} catch {
|
|
@@ -4412,8 +3914,8 @@ function createSessionIdleHook(client, tracker) {
|
|
|
4412
3914
|
}
|
|
4413
3915
|
|
|
4414
3916
|
// src/hooks/compaction-hook.ts
|
|
4415
|
-
import { existsSync as
|
|
4416
|
-
import { join as
|
|
3917
|
+
import { existsSync as existsSync28, readFileSync as readFileSync27 } from "fs";
|
|
3918
|
+
import { join as join26 } from "path";
|
|
4417
3919
|
var STRUCTURED_SUMMARY_PROMPT = `
|
|
4418
3920
|
When summarizing this session, you MUST include the following sections:
|
|
4419
3921
|
|
|
@@ -4454,10 +3956,10 @@ For each: agent name, status, description, session_id.
|
|
|
4454
3956
|
var _lastInjected = new Map;
|
|
4455
3957
|
function readPlanningState2(directory) {
|
|
4456
3958
|
const sp = statePath(directory);
|
|
4457
|
-
if (!
|
|
3959
|
+
if (!existsSync28(sp))
|
|
4458
3960
|
return null;
|
|
4459
3961
|
try {
|
|
4460
|
-
const content =
|
|
3962
|
+
const content = readFileSync27(sp, "utf-8");
|
|
4461
3963
|
const parsed = parseState(content);
|
|
4462
3964
|
const version = typeof parsed.summaryVersion === "number" ? parsed.summaryVersion : 0;
|
|
4463
3965
|
return { content: content.slice(0, 1500), version };
|
|
@@ -4486,15 +3988,15 @@ function createCompactionHook(ctx, tracker) {
|
|
|
4486
3988
|
sections.push(`_State unchanged since last compaction. summaryVersion=${currentStateVersion}_`);
|
|
4487
3989
|
sections.push("");
|
|
4488
3990
|
}
|
|
4489
|
-
const indexPath2 =
|
|
4490
|
-
if (indexChanged &&
|
|
3991
|
+
const indexPath2 = join26(ctx.directory, ".planning", "CODEBASE_INDEX.md");
|
|
3992
|
+
if (indexChanged && existsSync28(indexPath2)) {
|
|
4491
3993
|
try {
|
|
4492
|
-
const indexContent =
|
|
3994
|
+
const indexContent = readFileSync27(indexPath2, "utf-8");
|
|
4493
3995
|
const indexSummary = "\n## Codebase Index\n```\n" + indexContent.slice(0, 800) + "\n```\n";
|
|
4494
3996
|
sections.push(indexSummary);
|
|
4495
3997
|
sections.push("");
|
|
4496
3998
|
} catch {}
|
|
4497
|
-
} else if (
|
|
3999
|
+
} else if (existsSync28(indexPath2)) {
|
|
4498
4000
|
sections.push(`## Codebase Index (unchanged, v${currentIndexVersion})`);
|
|
4499
4001
|
sections.push(`_Index unchanged since last compaction. summaryVersion=${currentIndexVersion}_`);
|
|
4500
4002
|
sections.push("");
|
|
@@ -4545,12 +4047,9 @@ var ALWAYS_ALLOWED = new Set([
|
|
|
4545
4047
|
"council",
|
|
4546
4048
|
"planning-state",
|
|
4547
4049
|
"codebase-state",
|
|
4548
|
-
"workspace-state",
|
|
4549
4050
|
"repo-memory",
|
|
4550
4051
|
"decision-trace",
|
|
4551
4052
|
"policy-engine",
|
|
4552
|
-
"context-generator",
|
|
4553
|
-
"create-skill",
|
|
4554
4053
|
"reflect"
|
|
4555
4054
|
]);
|
|
4556
4055
|
function isDelegationTool(name) {
|
|
@@ -4661,7 +4160,7 @@ async function runAutoLearner(client, directory, appLog) {
|
|
|
4661
4160
|
parts: [
|
|
4662
4161
|
{
|
|
4663
4162
|
type: "text",
|
|
4664
|
-
text: "Run your automated self-improvement routine: call `reflect`, " + "identify patterns, and
|
|
4163
|
+
text: "Run your automated self-improvement routine: call `reflect`, " + "identify patterns, and write a skill markdown file under `src/skills/<name>/SKILL.md` for each one. " + "Complete silently without asking for input."
|
|
4665
4164
|
}
|
|
4666
4165
|
],
|
|
4667
4166
|
tools: { question: false }
|
|
@@ -4895,8 +4394,8 @@ Please advise.
|
|
|
4895
4394
|
## Self-Learning
|
|
4896
4395
|
|
|
4897
4396
|
When a task required unusual human guidance, a novel solution strategy, or exposed a knowledge gap:
|
|
4898
|
-
1. After the task completes successfully,
|
|
4899
|
-
2. Use a descriptive kebab-case name, a one-sentence description, and structured Markdown content
|
|
4397
|
+
1. After the task completes successfully, write a new skill markdown file under \`src/skills/<name>/SKILL.md\` to capture the pattern
|
|
4398
|
+
2. Use a descriptive kebab-case name for the directory, a one-sentence description in the frontmatter, and structured Markdown content
|
|
4900
4399
|
3. Include: When to Activate, Steps, Examples, and Pitfalls sections
|
|
4901
4400
|
|
|
4902
4401
|
Do NOT create a skill for routine tasks. Only capture genuinely novel or reusable patterns.`;
|
|
@@ -7042,7 +6541,6 @@ You receive a structured context with:
|
|
|
7042
6541
|
- \`file_path\`: optional specific file being changed
|
|
7043
6542
|
- \`trust_score\`: patch trust score (0–100; 80+ = safe, 40–79 = review-required, <40 = high-risk)
|
|
7044
6543
|
- \`trust_signals\`: list of risk signals from the patch trust scorer
|
|
7045
|
-
- \`volatile_zones\`: paths marked as volatile or critical in VOLATILITY.json
|
|
7046
6544
|
- \`prior_failures\`: failure entries from FAILURES.json that match this change
|
|
7047
6545
|
- \`regression_categories\`: predicted regression categories for this change
|
|
7048
6546
|
- \`confidence\`: system confidence score (0–100; based on how much codebase context data exists)
|
|
@@ -7501,7 +6999,7 @@ var AUTO_LEARNER_PROMPT = `You run automatically after a coding session to captu
|
|
|
7501
6999
|
- Novel solutions that took non-obvious reasoning
|
|
7502
7000
|
- Recurring tool sequences that indicate a reusable workflow
|
|
7503
7001
|
- Knowledge gaps that had to be worked out from scratch
|
|
7504
|
-
3. For each valuable pattern,
|
|
7002
|
+
3. For each valuable pattern, write a skill markdown file under \`src/skills/<name>/SKILL.md\` immediately.
|
|
7505
7003
|
4. If nothing is worth capturing, output exactly: "No new skills identified."
|
|
7506
7004
|
5. End with a one-line summary: "Auto-learn complete: N skill(s) created."
|
|
7507
7005
|
|
|
@@ -7816,7 +7314,6 @@ function createAgent(name, model, customPrompt, customAppendPrompt) {
|
|
|
7816
7314
|
case "supervisor":
|
|
7817
7315
|
return createSupervisorAgent(model, customPrompt, customAppendPrompt);
|
|
7818
7316
|
default:
|
|
7819
|
-
console.warn(`[flowdeck] Unknown agent: ${name}`);
|
|
7820
7317
|
return;
|
|
7821
7318
|
}
|
|
7822
7319
|
}
|
|
@@ -7866,12 +7363,9 @@ var CONTRACTS = [
|
|
|
7866
7363
|
"council",
|
|
7867
7364
|
"planning-state",
|
|
7868
7365
|
"codebase-state",
|
|
7869
|
-
"workspace-state",
|
|
7870
7366
|
"repo-memory",
|
|
7871
7367
|
"decision-trace",
|
|
7872
7368
|
"policy-engine",
|
|
7873
|
-
"context-generator",
|
|
7874
|
-
"create-skill",
|
|
7875
7369
|
"reflect"
|
|
7876
7370
|
],
|
|
7877
7371
|
forbiddenActions: [
|
|
@@ -7906,7 +7400,7 @@ var CONTRACTS = [
|
|
|
7906
7400
|
allowedTaskTypes: ["planning", "task-breakdown", "step-decomposition"],
|
|
7907
7401
|
requiredInputs: ["task description or STATE.md"],
|
|
7908
7402
|
expectedOutputFields: ["steps", "phase"],
|
|
7909
|
-
allowedTools: ["read", "glob", "grep", "planning-state"
|
|
7403
|
+
allowedTools: ["read", "glob", "grep", "planning-state"],
|
|
7910
7404
|
forbiddenActions: [
|
|
7911
7405
|
"write source files",
|
|
7912
7406
|
"run bash commands",
|
|
@@ -8422,7 +7916,6 @@ function runSupervisorReview(directory, targetName, ctx = {}, clarificationQuest
|
|
|
8422
7916
|
reviewPhase,
|
|
8423
7917
|
timestamp: timestamp2
|
|
8424
7918
|
};
|
|
8425
|
-
_emitTelemetry(directory, decision2, ctx);
|
|
8426
7919
|
return decision2;
|
|
8427
7920
|
}
|
|
8428
7921
|
const policyResult = targetType === "command" ? checkCommandPolicy(targetName, ctx) : checkAgentPolicy(targetName, ctx);
|
|
@@ -8444,7 +7937,6 @@ function runSupervisorReview(directory, targetName, ctx = {}, clarificationQuest
|
|
|
8444
7937
|
timestamp: timestamp2,
|
|
8445
7938
|
...escalationQuestion ? { clarificationQuestion: escalationQuestion } : {}
|
|
8446
7939
|
};
|
|
8447
|
-
_emitTelemetry(directory, supervisorDecision, ctx);
|
|
8448
7940
|
return supervisorDecision;
|
|
8449
7941
|
}
|
|
8450
7942
|
function shouldProceed(decision, mode, canBlock) {
|
|
@@ -8457,33 +7949,12 @@ function shouldProceed(decision, mode, canBlock) {
|
|
|
8457
7949
|
}
|
|
8458
7950
|
return decision.decision !== "block" || decision.confidenceScore > 0.3;
|
|
8459
7951
|
}
|
|
8460
|
-
function _emitTelemetry(directory, decision, ctx) {
|
|
8461
|
-
try {
|
|
8462
|
-
appendEvent2(directory, {
|
|
8463
|
-
session_id: ctx.session_id ?? "session-0",
|
|
8464
|
-
run_id: ctx.run_id ?? "unknown",
|
|
8465
|
-
event: "supervisor.review",
|
|
8466
|
-
agent: "supervisor",
|
|
8467
|
-
status: decision.decision === "approve" ? "ok" : decision.decision === "block" ? "blocked" : decision.decision === "escalate" ? "approved" : "ok",
|
|
8468
|
-
meta: {
|
|
8469
|
-
targetName: decision.targetName,
|
|
8470
|
-
targetType: decision.targetType,
|
|
8471
|
-
exists: decision.exists,
|
|
8472
|
-
decision: decision.decision,
|
|
8473
|
-
confidenceScore: decision.confidenceScore,
|
|
8474
|
-
riskFlags: decision.riskFlags,
|
|
8475
|
-
missingRequirements: decision.missingRequirements,
|
|
8476
|
-
reviewPhase: decision.reviewPhase
|
|
8477
|
-
}
|
|
8478
|
-
});
|
|
8479
|
-
} catch {}
|
|
8480
|
-
}
|
|
8481
7952
|
|
|
8482
7953
|
// src/index.ts
|
|
8483
7954
|
function lazyLoadRulePaths(projectRoot) {
|
|
8484
|
-
const __dir =
|
|
8485
|
-
const rulesDir =
|
|
8486
|
-
if (!
|
|
7955
|
+
const __dir = dirname3(fileURLToPath2(import.meta.url));
|
|
7956
|
+
const rulesDir = join27(__dir, "..", "src", "rules");
|
|
7957
|
+
if (!existsSync29(rulesDir))
|
|
8487
7958
|
return { paths: [], diagnostics: "[LazyRuleLoader] rules directory not found" };
|
|
8488
7959
|
const detectedLanguages = detectProjectLanguages(projectRoot);
|
|
8489
7960
|
const paths = getStartupRulePaths(rulesDir, detectedLanguages);
|
|
@@ -8492,17 +7963,17 @@ function lazyLoadRulePaths(projectRoot) {
|
|
|
8492
7963
|
return { paths, diagnostics };
|
|
8493
7964
|
}
|
|
8494
7965
|
function loadCommands() {
|
|
8495
|
-
const __dir =
|
|
8496
|
-
const commandsDir =
|
|
8497
|
-
if (!
|
|
7966
|
+
const __dir = dirname3(fileURLToPath2(import.meta.url));
|
|
7967
|
+
const commandsDir = join27(__dir, "..", "src", "commands");
|
|
7968
|
+
if (!existsSync29(commandsDir))
|
|
8498
7969
|
return {};
|
|
8499
7970
|
const commands = {};
|
|
8500
7971
|
try {
|
|
8501
|
-
for (const file of
|
|
7972
|
+
for (const file of readdirSync4(commandsDir)) {
|
|
8502
7973
|
if (!file.endsWith(".md"))
|
|
8503
7974
|
continue;
|
|
8504
7975
|
const name = basename2(file, ".md");
|
|
8505
|
-
const raw =
|
|
7976
|
+
const raw = readFileSync28(join27(commandsDir, file), "utf-8");
|
|
8506
7977
|
let description;
|
|
8507
7978
|
let template = raw;
|
|
8508
7979
|
const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
|
|
@@ -8520,10 +7991,8 @@ function loadCommands() {
|
|
|
8520
7991
|
var plugin = async (input, _options) => {
|
|
8521
7992
|
const { directory, client, worktree } = input;
|
|
8522
7993
|
const appLog = (msg) => client.app.log({ body: { service: "flowdeck", level: "info", message: msg } }).catch(() => {});
|
|
8523
|
-
const
|
|
8524
|
-
const
|
|
8525
|
-
const runPipelineTool = createRunPipelineTool(client, activityReporter);
|
|
8526
|
-
const delegateTool = createDelegateTool(client, activityReporter);
|
|
7994
|
+
const runPipelineTool = createRunPipelineTool(client);
|
|
7995
|
+
const delegateTool = createDelegateTool(client);
|
|
8527
7996
|
const councilTool = createCouncilTool(client);
|
|
8528
7997
|
const fileTracker = new SessionFileTracker;
|
|
8529
7998
|
const { fileEdited, fileWatcherUpdated } = createFileTrackerHooks(fileTracker);
|
|
@@ -8534,6 +8003,7 @@ var plugin = async (input, _options) => {
|
|
|
8534
8003
|
const compactionHook = createCompactionHook({ directory }, fileTracker);
|
|
8535
8004
|
const orchestratorGuard = new OrchestratorGuard;
|
|
8536
8005
|
const autoLearnHook = createAutoLearnHook(client, fileTracker, directory, appLog);
|
|
8006
|
+
const eventLog = createEventLogHooks(appLog);
|
|
8537
8007
|
const notifCtrl = new NotificationController(undefined, appLog);
|
|
8538
8008
|
const agentConfigs = getAgentConfigs({});
|
|
8539
8009
|
const mcps = createFlowDeckMcps();
|
|
@@ -8587,8 +8057,8 @@ var plugin = async (input, _options) => {
|
|
|
8587
8057
|
}
|
|
8588
8058
|
}
|
|
8589
8059
|
}
|
|
8590
|
-
const skillsDir =
|
|
8591
|
-
if (
|
|
8060
|
+
const skillsDir = join27(dirname3(fileURLToPath2(import.meta.url)), "..", "src", "skills");
|
|
8061
|
+
if (existsSync29(skillsDir)) {
|
|
8592
8062
|
const cfgAny = cfg;
|
|
8593
8063
|
if (!cfgAny.skills || typeof cfgAny.skills !== "object") {
|
|
8594
8064
|
cfgAny.skills = { paths: [] };
|
|
@@ -8617,18 +8087,14 @@ var plugin = async (input, _options) => {
|
|
|
8617
8087
|
tool: {
|
|
8618
8088
|
"planning-state": planningStateTool,
|
|
8619
8089
|
"codebase-state": codebaseStateTool,
|
|
8620
|
-
"workspace-state": workspaceStateTool,
|
|
8621
8090
|
"run-pipeline": runPipelineTool,
|
|
8622
8091
|
delegate: delegateTool,
|
|
8623
8092
|
"repo-memory": repoMemoryTool,
|
|
8624
8093
|
"failure-replay": failureReplayTool,
|
|
8625
8094
|
"decision-trace": decisionTraceTool,
|
|
8626
|
-
"volatility-map": volatilityMapTool,
|
|
8627
8095
|
"policy-engine": policyEngineTool,
|
|
8628
8096
|
"hash-edit": hashEditTool,
|
|
8629
8097
|
council: councilTool,
|
|
8630
|
-
"context-generator": contextGeneratorTool,
|
|
8631
|
-
"create-skill": createSkillTool,
|
|
8632
8098
|
reflect: reflectTool,
|
|
8633
8099
|
codegraph: codegraphTool,
|
|
8634
8100
|
"load-rules": loadRulesTool,
|
|
@@ -8641,17 +8107,18 @@ var plugin = async (input, _options) => {
|
|
|
8641
8107
|
"file.watcher.updated": fileWatcherUpdated,
|
|
8642
8108
|
"experimental.session.compacting": compactionHook,
|
|
8643
8109
|
"command.execute.before": async (input2, _output) => {
|
|
8644
|
-
activityReporter.reportCommandStarted(input2.command);
|
|
8645
8110
|
lastExecutedCommand = input2.command;
|
|
8646
8111
|
},
|
|
8647
8112
|
"permission.ask": async (input2, _output) => {
|
|
8648
8113
|
notifyPermissionNeeded(input2.title);
|
|
8649
|
-
activityReporter.reportWaitingForApproval(input2.title);
|
|
8650
8114
|
},
|
|
8651
8115
|
event: async ({ event }) => {
|
|
8652
8116
|
const type = event?.type ?? "";
|
|
8653
8117
|
if (type === "session.created" || type === "session.started") {
|
|
8654
8118
|
await sessionStartHook({ directory });
|
|
8119
|
+
if (type === "session.created") {
|
|
8120
|
+
await eventLog.session({ directory }, event);
|
|
8121
|
+
}
|
|
8655
8122
|
}
|
|
8656
8123
|
if (type === "command.executed") {
|
|
8657
8124
|
const commandName = event?.properties?.name ?? "";
|
|
@@ -8662,9 +8129,9 @@ var plugin = async (input, _options) => {
|
|
|
8662
8129
|
await contextMonitor.event({ event });
|
|
8663
8130
|
orchestratorGuard.onEvent(event);
|
|
8664
8131
|
if (type === "session.idle") {
|
|
8132
|
+
await eventLog.session({ directory }, event);
|
|
8665
8133
|
const hasEdits = fileTracker.getEditedPaths().length > 0;
|
|
8666
8134
|
if (lastExecutedCommand) {
|
|
8667
|
-
activityReporter.reportCommandCompleted(lastExecutedCommand, hasEdits);
|
|
8668
8135
|
lastExecutedCommand = null;
|
|
8669
8136
|
}
|
|
8670
8137
|
notifCtrl.onSessionIdle(hasEdits);
|
|
@@ -8676,6 +8143,7 @@ var plugin = async (input, _options) => {
|
|
|
8676
8143
|
}
|
|
8677
8144
|
}
|
|
8678
8145
|
if (type === "session.error") {
|
|
8146
|
+
await eventLog.session({ directory }, event);
|
|
8679
8147
|
lastExecutedCommand = null;
|
|
8680
8148
|
const err = event?.properties?.error;
|
|
8681
8149
|
const errorMsg = (err && typeof err === "object" && "message" in err ? String(err.message) : undefined) ?? (typeof err === "string" ? err : undefined) ?? "An unexpected error occurred";
|
|
@@ -8721,15 +8189,15 @@ var plugin = async (input, _options) => {
|
|
|
8721
8189
|
}
|
|
8722
8190
|
}
|
|
8723
8191
|
}
|
|
8724
|
-
await telemetryHook({ directory }, toolInput, toolOutput, activityReporter);
|
|
8725
8192
|
await approvalHook({ directory }, toolInput, toolOutput);
|
|
8726
8193
|
await guardRailsHook({ directory }, toolInput, toolOutput);
|
|
8727
8194
|
await toolGuardHook({ directory }, toolInput, toolOutput);
|
|
8728
8195
|
await patchTrustHook({ directory }, toolInput, toolOutput);
|
|
8729
8196
|
await decisionTraceHook({ directory }, toolInput, toolOutput);
|
|
8197
|
+
await eventLog.before({ directory }, toolInput, toolOutput);
|
|
8730
8198
|
},
|
|
8731
8199
|
"tool.execute.after": async (toolInput, toolOutput) => {
|
|
8732
|
-
await
|
|
8200
|
+
await eventLog.after({ directory }, toolInput, toolOutput);
|
|
8733
8201
|
const afterToolName = toolInput.tool ?? toolInput.name ?? "";
|
|
8734
8202
|
if (afterToolName === "delegate" || afterToolName === "run-pipeline") {
|
|
8735
8203
|
try {
|