@dv.nghiem/flowdeck 0.4.4 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/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 +12 -0
- package/dist/hooks/event-log-hook.d.ts.map +1 -0
- 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 +622 -1145
- package/dist/lib/impact-radar.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,23 +1137,23 @@ 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
1158
|
} catch {
|
|
1532
1159
|
console.warn(`[flowdeck] Failed to load config from ${configPath}`);
|
|
@@ -1611,19 +1238,19 @@ function extractText2(parts) {
|
|
|
1611
1238
|
return parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join(`
|
|
1612
1239
|
`);
|
|
1613
1240
|
}
|
|
1614
|
-
function createDelegateTool(client
|
|
1615
|
-
return
|
|
1241
|
+
function createDelegateTool(client) {
|
|
1242
|
+
return tool4({
|
|
1616
1243
|
description: "Delegate a task to a single agent via a child session. Returns the agent's output.",
|
|
1617
1244
|
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:
|
|
1245
|
+
agent: tool4.schema.string(),
|
|
1246
|
+
prompt: tool4.schema.string(),
|
|
1247
|
+
context: tool4.schema.string().optional(),
|
|
1248
|
+
task_type: tool4.schema.string().optional(),
|
|
1249
|
+
retry_attempts: tool4.schema.number().optional().default(1),
|
|
1250
|
+
safe_to_cache: tool4.schema.boolean().optional().default(false),
|
|
1251
|
+
cache_ttl_ms: tool4.schema.number().optional(),
|
|
1252
|
+
workflow_id: tool4.schema.string().optional(),
|
|
1253
|
+
stage: tool4.schema.string().optional()
|
|
1627
1254
|
},
|
|
1628
1255
|
async execute(args, context) {
|
|
1629
1256
|
const startTime = Date.now();
|
|
@@ -1642,17 +1269,13 @@ function createDelegateTool(client, reporter) {
|
|
|
1642
1269
|
---
|
|
1643
1270
|
|
|
1644
1271
|
${args.prompt}` : args.prompt;
|
|
1645
|
-
reporter?.reportToolStarted("delegate", summarize(args.prompt, 100), {
|
|
1646
|
-
agent: args.agent,
|
|
1647
|
-
stage: args.stage
|
|
1648
|
-
});
|
|
1649
1272
|
const safe_to_cache = args.safe_to_cache === true && CACHEABLE_AGENTS.has(args.agent);
|
|
1650
1273
|
let stateVersion = 0;
|
|
1651
1274
|
let indexVersion = 0;
|
|
1652
1275
|
if (safe_to_cache) {
|
|
1653
1276
|
const index = readCodebaseIndex(context.directory);
|
|
1654
1277
|
const sp = statePath(context.directory);
|
|
1655
|
-
const rawState =
|
|
1278
|
+
const rawState = existsSync10(sp) ? readFileSync10(sp, "utf-8") : "";
|
|
1656
1279
|
const state = rawState ? parseState(rawState) : {};
|
|
1657
1280
|
stateVersion = typeof state.summaryVersion === "number" ? state.summaryVersion : 0;
|
|
1658
1281
|
indexVersion = typeof index.summaryVersion === "number" ? index.summaryVersion : 0;
|
|
@@ -1661,7 +1284,6 @@ ${args.prompt}` : args.prompt;
|
|
|
1661
1284
|
if (metricsWorkflowId) {
|
|
1662
1285
|
recordCacheHit(context.directory, metricsWorkflowId, metricsStage, fullPrompt, args.agent, agentModel);
|
|
1663
1286
|
}
|
|
1664
|
-
reporter?.reportCacheHit("delegate", args.agent);
|
|
1665
1287
|
return JSON.stringify({
|
|
1666
1288
|
agent: args.agent,
|
|
1667
1289
|
success: true,
|
|
@@ -1719,15 +1341,10 @@ ${args.prompt}` : args.prompt;
|
|
|
1719
1341
|
recordRetryCall(context.directory, metricsWorkflowId, metricsStage, fullPromptForSession, "", args.agent, Date.now() - attemptStart, agentModel, retryCostUsd);
|
|
1720
1342
|
}
|
|
1721
1343
|
retriesUsed++;
|
|
1722
|
-
reporter?.reportToolRetried("delegate", retriesUsed, "prompt response indicated retry", { agent: args.agent });
|
|
1723
1344
|
}
|
|
1724
1345
|
if (!promptRes || promptRes.error) {
|
|
1725
1346
|
const errMsg = `Prompt failed: ${promptRes?.error?.detail ?? "unknown"}`;
|
|
1726
1347
|
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
1348
|
return JSON.stringify({
|
|
1732
1349
|
agent: args.agent,
|
|
1733
1350
|
session_id: childId,
|
|
@@ -1743,10 +1360,6 @@ ${args.prompt}` : args.prompt;
|
|
|
1743
1360
|
if (info?.error) {
|
|
1744
1361
|
const errMsg = `Agent error: ${JSON.stringify(info.error)}`;
|
|
1745
1362
|
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
1363
|
return JSON.stringify({
|
|
1751
1364
|
agent: args.agent,
|
|
1752
1365
|
session_id: childId,
|
|
@@ -1766,10 +1379,6 @@ ${args.prompt}` : args.prompt;
|
|
|
1766
1379
|
const costUsd = agentModel ? estimateCostUSD(agentModel, inputTokens, outputTokens) : undefined;
|
|
1767
1380
|
recordModelCall(context.directory, metricsWorkflowId, metricsStage, fullPromptForSession, output, args.agent, Date.now() - startTime, agentModel, costUsd);
|
|
1768
1381
|
}
|
|
1769
|
-
reporter?.reportToolCompleted("delegate", Date.now() - startTime, summarize(output, 80), {
|
|
1770
|
-
agent: args.agent,
|
|
1771
|
-
retry_count: retriesUsed
|
|
1772
|
-
});
|
|
1773
1382
|
if (safe_to_cache && output) {
|
|
1774
1383
|
setCached(context.directory, args.agent, fullPromptForSession, args.context ?? "", stateVersion, indexVersion, output, true, args.cache_ttl_ms);
|
|
1775
1384
|
}
|
|
@@ -1788,53 +1397,53 @@ ${args.prompt}` : args.prompt;
|
|
|
1788
1397
|
}
|
|
1789
1398
|
|
|
1790
1399
|
// src/tools/repo-memory.ts
|
|
1791
|
-
import { tool as
|
|
1792
|
-
import { readFileSync as
|
|
1793
|
-
import { join as
|
|
1400
|
+
import { tool as tool5 } from "@opencode-ai/plugin";
|
|
1401
|
+
import { readFileSync as readFileSync11, writeFileSync as writeFileSync7, existsSync as existsSync11, mkdirSync as mkdirSync6 } from "fs";
|
|
1402
|
+
import { join as join10 } from "path";
|
|
1794
1403
|
var MEMORY_FILE = "MEMORY.json";
|
|
1795
1404
|
function memoryPath(directory) {
|
|
1796
|
-
return
|
|
1405
|
+
return join10(codebaseDir(directory), MEMORY_FILE);
|
|
1797
1406
|
}
|
|
1798
1407
|
function emptyMemory() {
|
|
1799
1408
|
return { version: "1.0", last_updated: new Date().toISOString(), nodes: {} };
|
|
1800
1409
|
}
|
|
1801
1410
|
function readMemory(directory) {
|
|
1802
1411
|
const p = memoryPath(directory);
|
|
1803
|
-
if (!
|
|
1412
|
+
if (!existsSync11(p))
|
|
1804
1413
|
return emptyMemory();
|
|
1805
1414
|
try {
|
|
1806
|
-
return JSON.parse(
|
|
1415
|
+
return JSON.parse(readFileSync11(p, "utf-8"));
|
|
1807
1416
|
} catch {
|
|
1808
1417
|
return emptyMemory();
|
|
1809
1418
|
}
|
|
1810
1419
|
}
|
|
1811
1420
|
function writeMemory(directory, memory) {
|
|
1812
1421
|
const base = codebaseDir(directory);
|
|
1813
|
-
if (!
|
|
1422
|
+
if (!existsSync11(base))
|
|
1814
1423
|
mkdirSync6(base, { recursive: true });
|
|
1815
1424
|
memory.last_updated = new Date().toISOString();
|
|
1816
|
-
|
|
1425
|
+
writeFileSync7(memoryPath(directory), JSON.stringify(memory, null, 2), "utf-8");
|
|
1817
1426
|
}
|
|
1818
|
-
var repoMemoryTool =
|
|
1427
|
+
var repoMemoryTool = tool5({
|
|
1819
1428
|
description: "Repo Memory Graph: read/write/query persistent architecture graph in .codebase/MEMORY.json (modules, dependencies, ownership, bug history, conventions)",
|
|
1820
1429
|
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:
|
|
1430
|
+
action: tool5.schema.enum(["read", "write_node", "query", "delete_node"]),
|
|
1431
|
+
node_id: tool5.schema.string().optional(),
|
|
1432
|
+
node: tool5.schema.object({
|
|
1433
|
+
type: tool5.schema.enum(["module", "service", "api", "schema", "config"]),
|
|
1434
|
+
path: tool5.schema.string(),
|
|
1435
|
+
owner: tool5.schema.string().optional(),
|
|
1436
|
+
tags: tool5.schema.array(tool5.schema.string()),
|
|
1437
|
+
dependencies: tool5.schema.array(tool5.schema.string()),
|
|
1438
|
+
dependents: tool5.schema.array(tool5.schema.string()),
|
|
1439
|
+
bug_history: tool5.schema.array(tool5.schema.string()),
|
|
1440
|
+
conventions: tool5.schema.array(tool5.schema.string())
|
|
1832
1441
|
}).optional(),
|
|
1833
|
-
query:
|
|
1834
|
-
type:
|
|
1835
|
-
owner:
|
|
1836
|
-
tag:
|
|
1837
|
-
path_prefix:
|
|
1442
|
+
query: tool5.schema.object({
|
|
1443
|
+
type: tool5.schema.enum(["module", "service", "api", "schema", "config"]).optional(),
|
|
1444
|
+
owner: tool5.schema.string().optional(),
|
|
1445
|
+
tag: tool5.schema.string().optional(),
|
|
1446
|
+
path_prefix: tool5.schema.string().optional()
|
|
1838
1447
|
}).optional()
|
|
1839
1448
|
},
|
|
1840
1449
|
async execute(args, context) {
|
|
@@ -1889,50 +1498,50 @@ var repoMemoryTool = tool6({
|
|
|
1889
1498
|
});
|
|
1890
1499
|
|
|
1891
1500
|
// src/tools/failure-replay.ts
|
|
1892
|
-
import { tool as
|
|
1893
|
-
import { readFileSync as
|
|
1894
|
-
import { join as
|
|
1501
|
+
import { tool as tool6 } from "@opencode-ai/plugin";
|
|
1502
|
+
import { readFileSync as readFileSync12, writeFileSync as writeFileSync8, existsSync as existsSync12, mkdirSync as mkdirSync7 } from "fs";
|
|
1503
|
+
import { join as join11 } from "path";
|
|
1895
1504
|
var FAILURES_FILE = "FAILURES.json";
|
|
1896
1505
|
function failuresPath(directory) {
|
|
1897
|
-
return
|
|
1506
|
+
return join11(codebaseDir(directory), FAILURES_FILE);
|
|
1898
1507
|
}
|
|
1899
1508
|
function readStore(directory) {
|
|
1900
1509
|
const p = failuresPath(directory);
|
|
1901
|
-
if (!
|
|
1510
|
+
if (!existsSync12(p))
|
|
1902
1511
|
return { version: "1.0", last_updated: new Date().toISOString(), entries: [] };
|
|
1903
1512
|
try {
|
|
1904
|
-
return JSON.parse(
|
|
1513
|
+
return JSON.parse(readFileSync12(p, "utf-8"));
|
|
1905
1514
|
} catch {
|
|
1906
1515
|
return { version: "1.0", last_updated: new Date().toISOString(), entries: [] };
|
|
1907
1516
|
}
|
|
1908
1517
|
}
|
|
1909
1518
|
function writeStore(directory, store) {
|
|
1910
1519
|
const base = codebaseDir(directory);
|
|
1911
|
-
if (!
|
|
1520
|
+
if (!existsSync12(base))
|
|
1912
1521
|
mkdirSync7(base, { recursive: true });
|
|
1913
1522
|
store.last_updated = new Date().toISOString();
|
|
1914
|
-
|
|
1523
|
+
writeFileSync8(failuresPath(directory), JSON.stringify(store, null, 2), "utf-8");
|
|
1915
1524
|
}
|
|
1916
|
-
var failureReplayTool =
|
|
1525
|
+
var failureReplayTool = tool6({
|
|
1917
1526
|
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
1527
|
args: {
|
|
1919
|
-
action:
|
|
1920
|
-
entry:
|
|
1921
|
-
id:
|
|
1922
|
-
type:
|
|
1923
|
-
description:
|
|
1924
|
-
affected_paths:
|
|
1925
|
-
root_cause:
|
|
1926
|
-
fix_applied:
|
|
1927
|
-
tags:
|
|
1528
|
+
action: tool6.schema.enum(["record", "query", "list", "mark_resolved"]),
|
|
1529
|
+
entry: tool6.schema.object({
|
|
1530
|
+
id: tool6.schema.string(),
|
|
1531
|
+
type: tool6.schema.enum(["reverted_commit", "failed_deployment", "flaky_test", "bug_fix", "build_failure"]),
|
|
1532
|
+
description: tool6.schema.string(),
|
|
1533
|
+
affected_paths: tool6.schema.array(tool6.schema.string()),
|
|
1534
|
+
root_cause: tool6.schema.string().optional(),
|
|
1535
|
+
fix_applied: tool6.schema.string().optional(),
|
|
1536
|
+
tags: tool6.schema.array(tool6.schema.string())
|
|
1928
1537
|
}).optional(),
|
|
1929
|
-
query:
|
|
1930
|
-
type:
|
|
1931
|
-
path_prefix:
|
|
1932
|
-
tag:
|
|
1933
|
-
limit:
|
|
1538
|
+
query: tool6.schema.object({
|
|
1539
|
+
type: tool6.schema.enum(["reverted_commit", "failed_deployment", "flaky_test", "bug_fix", "build_failure"]).optional(),
|
|
1540
|
+
path_prefix: tool6.schema.string().optional(),
|
|
1541
|
+
tag: tool6.schema.string().optional(),
|
|
1542
|
+
limit: tool6.schema.number().optional()
|
|
1934
1543
|
}).optional(),
|
|
1935
|
-
entry_id:
|
|
1544
|
+
entry_id: tool6.schema.string().optional()
|
|
1936
1545
|
},
|
|
1937
1546
|
async execute(args, context) {
|
|
1938
1547
|
const dir = context.directory ?? process.cwd();
|
|
@@ -1994,18 +1603,18 @@ var failureReplayTool = tool7({
|
|
|
1994
1603
|
});
|
|
1995
1604
|
|
|
1996
1605
|
// src/tools/decision-trace.ts
|
|
1997
|
-
import { tool as
|
|
1998
|
-
import { readFileSync as
|
|
1999
|
-
import { join as
|
|
1606
|
+
import { tool as tool7 } from "@opencode-ai/plugin";
|
|
1607
|
+
import { readFileSync as readFileSync13, existsSync as existsSync13, mkdirSync as mkdirSync8, appendFileSync as appendFileSync2 } from "fs";
|
|
1608
|
+
import { join as join12 } from "path";
|
|
2000
1609
|
var DECISIONS_FILE = "DECISIONS.jsonl";
|
|
2001
1610
|
function decisionsPath(directory) {
|
|
2002
|
-
return
|
|
1611
|
+
return join12(codebaseDir(directory), DECISIONS_FILE);
|
|
2003
1612
|
}
|
|
2004
1613
|
function readDecisions(directory) {
|
|
2005
1614
|
const p = decisionsPath(directory);
|
|
2006
|
-
if (!
|
|
1615
|
+
if (!existsSync13(p))
|
|
2007
1616
|
return [];
|
|
2008
|
-
return
|
|
1617
|
+
return readFileSync13(p, "utf-8").split(`
|
|
2009
1618
|
`).filter((l) => l.trim()).map((l) => {
|
|
2010
1619
|
try {
|
|
2011
1620
|
return JSON.parse(l);
|
|
@@ -2014,29 +1623,29 @@ function readDecisions(directory) {
|
|
|
2014
1623
|
}
|
|
2015
1624
|
}).filter(Boolean);
|
|
2016
1625
|
}
|
|
2017
|
-
var decisionTraceTool =
|
|
1626
|
+
var decisionTraceTool = tool7({
|
|
2018
1627
|
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
1628
|
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:
|
|
1629
|
+
action: tool7.schema.enum(["record", "query", "get_for_file"]),
|
|
1630
|
+
entry: tool7.schema.object({
|
|
1631
|
+
id: tool7.schema.string(),
|
|
1632
|
+
file_path: tool7.schema.string(),
|
|
1633
|
+
change_type: tool7.schema.enum(["create", "edit", "delete", "refactor"]),
|
|
1634
|
+
rationale: tool7.schema.string(),
|
|
1635
|
+
evidence: tool7.schema.array(tool7.schema.string()),
|
|
1636
|
+
assumptions: tool7.schema.array(tool7.schema.string()),
|
|
1637
|
+
alternatives_considered: tool7.schema.array(tool7.schema.string()),
|
|
1638
|
+
risk_level: tool7.schema.enum(["low", "medium", "high"]),
|
|
1639
|
+
agent: tool7.schema.string().optional(),
|
|
1640
|
+
session_id: tool7.schema.string().optional()
|
|
2032
1641
|
}).optional(),
|
|
2033
|
-
query:
|
|
2034
|
-
file_path:
|
|
2035
|
-
change_type:
|
|
2036
|
-
risk_level:
|
|
2037
|
-
limit:
|
|
1642
|
+
query: tool7.schema.object({
|
|
1643
|
+
file_path: tool7.schema.string().optional(),
|
|
1644
|
+
change_type: tool7.schema.enum(["create", "edit", "delete", "refactor"]).optional(),
|
|
1645
|
+
risk_level: tool7.schema.enum(["low", "medium", "high"]).optional(),
|
|
1646
|
+
limit: tool7.schema.number().optional()
|
|
2038
1647
|
}).optional(),
|
|
2039
|
-
file_path:
|
|
1648
|
+
file_path: tool7.schema.string().optional()
|
|
2040
1649
|
},
|
|
2041
1650
|
async execute(args, context) {
|
|
2042
1651
|
const dir = context.directory ?? process.cwd();
|
|
@@ -2045,7 +1654,7 @@ var decisionTraceTool = tool8({
|
|
|
2045
1654
|
case "record": {
|
|
2046
1655
|
if (!args.entry)
|
|
2047
1656
|
return JSON.stringify({ error: "entry required" });
|
|
2048
|
-
if (!
|
|
1657
|
+
if (!existsSync13(base))
|
|
2049
1658
|
mkdirSync8(base, { recursive: true });
|
|
2050
1659
|
const entry = { ...args.entry, timestamp: new Date().toISOString() };
|
|
2051
1660
|
appendFileSync2(decisionsPath(dir), JSON.stringify(entry) + `
|
|
@@ -2078,162 +1687,54 @@ var decisionTraceTool = tool8({
|
|
|
2078
1687
|
}
|
|
2079
1688
|
});
|
|
2080
1689
|
|
|
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
1690
|
// src/tools/policy-engine.ts
|
|
2190
|
-
import { tool as
|
|
2191
|
-
import { readFileSync as
|
|
2192
|
-
import { join as
|
|
1691
|
+
import { tool as tool8 } from "@opencode-ai/plugin";
|
|
1692
|
+
import { readFileSync as readFileSync14, writeFileSync as writeFileSync10, existsSync as existsSync14, mkdirSync as mkdirSync9 } from "fs";
|
|
1693
|
+
import { join as join13 } from "path";
|
|
2193
1694
|
var POLICIES_FILE = "POLICIES.json";
|
|
2194
1695
|
function policiesPath(directory) {
|
|
2195
|
-
return
|
|
1696
|
+
return join13(codebaseDir(directory), POLICIES_FILE);
|
|
2196
1697
|
}
|
|
2197
|
-
function
|
|
1698
|
+
function readStore2(directory) {
|
|
2198
1699
|
const p = policiesPath(directory);
|
|
2199
|
-
if (!
|
|
1700
|
+
if (!existsSync14(p))
|
|
2200
1701
|
return { version: "1.0", last_updated: new Date().toISOString(), policies: [] };
|
|
2201
1702
|
try {
|
|
2202
|
-
return JSON.parse(
|
|
1703
|
+
return JSON.parse(readFileSync14(p, "utf-8"));
|
|
2203
1704
|
} catch {
|
|
2204
1705
|
return { version: "1.0", last_updated: new Date().toISOString(), policies: [] };
|
|
2205
1706
|
}
|
|
2206
1707
|
}
|
|
2207
|
-
function
|
|
1708
|
+
function writeStore2(directory, store) {
|
|
2208
1709
|
const base = codebaseDir(directory);
|
|
2209
|
-
if (!
|
|
2210
|
-
|
|
1710
|
+
if (!existsSync14(base))
|
|
1711
|
+
mkdirSync9(base, { recursive: true });
|
|
2211
1712
|
store.last_updated = new Date().toISOString();
|
|
2212
|
-
|
|
1713
|
+
writeFileSync10(policiesPath(directory), JSON.stringify(store, null, 2), "utf-8");
|
|
2213
1714
|
}
|
|
2214
|
-
var policyEngineTool =
|
|
1715
|
+
var policyEngineTool = tool8({
|
|
2215
1716
|
description: "Self-Healing Policy Engine: manage .codebase/POLICIES.json — add, list, query, toggle, and record violations of editing policies learned from past failures",
|
|
2216
1717
|
args: {
|
|
2217
|
-
action:
|
|
2218
|
-
policy:
|
|
2219
|
-
id:
|
|
2220
|
-
name:
|
|
2221
|
-
trigger:
|
|
2222
|
-
rule:
|
|
2223
|
-
source:
|
|
2224
|
-
failure_count:
|
|
1718
|
+
action: tool8.schema.enum(["list", "add", "record_violation", "toggle", "query"]),
|
|
1719
|
+
policy: tool8.schema.object({
|
|
1720
|
+
id: tool8.schema.string(),
|
|
1721
|
+
name: tool8.schema.string(),
|
|
1722
|
+
trigger: tool8.schema.string(),
|
|
1723
|
+
rule: tool8.schema.string(),
|
|
1724
|
+
source: tool8.schema.enum(["manual", "learned"]),
|
|
1725
|
+
failure_count: tool8.schema.number()
|
|
2225
1726
|
}).optional(),
|
|
2226
|
-
policy_id:
|
|
2227
|
-
active:
|
|
2228
|
-
query:
|
|
2229
|
-
source:
|
|
2230
|
-
active_only:
|
|
2231
|
-
trigger_contains:
|
|
1727
|
+
policy_id: tool8.schema.string().optional(),
|
|
1728
|
+
active: tool8.schema.boolean().optional(),
|
|
1729
|
+
query: tool8.schema.object({
|
|
1730
|
+
source: tool8.schema.enum(["manual", "learned"]).optional(),
|
|
1731
|
+
active_only: tool8.schema.boolean().optional(),
|
|
1732
|
+
trigger_contains: tool8.schema.string().optional()
|
|
2232
1733
|
}).optional()
|
|
2233
1734
|
},
|
|
2234
1735
|
async execute(args, context) {
|
|
2235
1736
|
const dir = context.directory ?? process.cwd();
|
|
2236
|
-
const store =
|
|
1737
|
+
const store = readStore2(dir);
|
|
2237
1738
|
switch (args.action) {
|
|
2238
1739
|
case "list": {
|
|
2239
1740
|
const active = store.policies.filter((p) => p.active);
|
|
@@ -2248,7 +1749,7 @@ var policyEngineTool = tool10({
|
|
|
2248
1749
|
} else {
|
|
2249
1750
|
store.policies.push({ ...args.policy, created_at: new Date().toISOString(), active: true });
|
|
2250
1751
|
}
|
|
2251
|
-
|
|
1752
|
+
writeStore2(dir, store);
|
|
2252
1753
|
return JSON.stringify({ success: true, id: args.policy.id });
|
|
2253
1754
|
}
|
|
2254
1755
|
case "record_violation": {
|
|
@@ -2259,7 +1760,7 @@ var policyEngineTool = tool10({
|
|
|
2259
1760
|
return JSON.stringify({ error: `Policy not found: ${args.policy_id}` });
|
|
2260
1761
|
policy.failure_count++;
|
|
2261
1762
|
policy.last_violated = new Date().toISOString();
|
|
2262
|
-
|
|
1763
|
+
writeStore2(dir, store);
|
|
2263
1764
|
return JSON.stringify({ success: true, policy_id: args.policy_id, failure_count: policy.failure_count });
|
|
2264
1765
|
}
|
|
2265
1766
|
case "toggle": {
|
|
@@ -2269,7 +1770,7 @@ var policyEngineTool = tool10({
|
|
|
2269
1770
|
if (!policy)
|
|
2270
1771
|
return JSON.stringify({ error: `Policy not found: ${args.policy_id}` });
|
|
2271
1772
|
policy.active = args.active !== undefined ? args.active : !policy.active;
|
|
2272
|
-
|
|
1773
|
+
writeStore2(dir, store);
|
|
2273
1774
|
return JSON.stringify({ success: true, policy_id: args.policy_id, active: policy.active });
|
|
2274
1775
|
}
|
|
2275
1776
|
case "query": {
|
|
@@ -2290,22 +1791,22 @@ var policyEngineTool = tool10({
|
|
|
2290
1791
|
});
|
|
2291
1792
|
|
|
2292
1793
|
// src/tools/hash-edit.ts
|
|
2293
|
-
import { tool as
|
|
2294
|
-
import { readFileSync as
|
|
1794
|
+
import { tool as tool9 } from "@opencode-ai/plugin";
|
|
1795
|
+
import { readFileSync as readFileSync15, writeFileSync as writeFileSync11 } from "fs";
|
|
2295
1796
|
import { createHash as createHash2 } from "crypto";
|
|
2296
|
-
var hashEditTool =
|
|
1797
|
+
var hashEditTool = tool9({
|
|
2297
1798
|
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
1799
|
args: {
|
|
2299
|
-
filePath:
|
|
2300
|
-
targetContent:
|
|
2301
|
-
expectedHash:
|
|
2302
|
-
replacementContent:
|
|
1800
|
+
filePath: tool9.schema.string(),
|
|
1801
|
+
targetContent: tool9.schema.string(),
|
|
1802
|
+
expectedHash: tool9.schema.string().optional(),
|
|
1803
|
+
replacementContent: tool9.schema.string()
|
|
2303
1804
|
},
|
|
2304
1805
|
async execute(args, context) {
|
|
2305
1806
|
const fullPath = args.filePath.startsWith("/") ? args.filePath : `${context.directory}/${args.filePath}`;
|
|
2306
1807
|
let content;
|
|
2307
1808
|
try {
|
|
2308
|
-
content =
|
|
1809
|
+
content = readFileSync15(fullPath, "utf-8");
|
|
2309
1810
|
} catch (e) {
|
|
2310
1811
|
return `Error: Could not read file ${args.filePath}`;
|
|
2311
1812
|
}
|
|
@@ -2319,17 +1820,17 @@ var hashEditTool = tool11({
|
|
|
2319
1820
|
}
|
|
2320
1821
|
}
|
|
2321
1822
|
const newContent = content.replace(args.targetContent, args.replacementContent);
|
|
2322
|
-
|
|
1823
|
+
writeFileSync11(fullPath, newContent, "utf-8");
|
|
2323
1824
|
return `Successfully updated ${args.filePath} using hash-anchored edit.`;
|
|
2324
1825
|
}
|
|
2325
1826
|
});
|
|
2326
1827
|
|
|
2327
1828
|
// src/tools/council.ts
|
|
2328
|
-
import { tool as
|
|
2329
|
-
import { appendFileSync as appendFileSync3, existsSync as
|
|
2330
|
-
import { join as
|
|
1829
|
+
import { tool as tool10 } from "@opencode-ai/plugin";
|
|
1830
|
+
import { appendFileSync as appendFileSync3, existsSync as existsSync15, mkdirSync as mkdirSync10 } from "fs";
|
|
1831
|
+
import { join as join14 } from "path";
|
|
2331
1832
|
import { createHash as createHash3 } from "crypto";
|
|
2332
|
-
import { readFileSync as
|
|
1833
|
+
import { readFileSync as readFileSync16 } from "fs";
|
|
2333
1834
|
var _councilCache = new Map;
|
|
2334
1835
|
var COUNCIL_CACHE_TTL_MS = 20 * 60 * 1000;
|
|
2335
1836
|
function councilCacheKey(task, agents, stateVersion, indexVersion) {
|
|
@@ -2350,20 +1851,20 @@ async function runWithConcurrencyLimit(tasks, limit) {
|
|
|
2350
1851
|
return results;
|
|
2351
1852
|
}
|
|
2352
1853
|
function createCouncilTool(client) {
|
|
2353
|
-
return
|
|
1854
|
+
return tool10({
|
|
2354
1855
|
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
1856
|
args: {
|
|
2356
|
-
task:
|
|
2357
|
-
agents:
|
|
2358
|
-
force_fresh:
|
|
2359
|
-
max_concurrency:
|
|
1857
|
+
task: tool10.schema.string(),
|
|
1858
|
+
agents: tool10.schema.array(tool10.schema.string()).optional(),
|
|
1859
|
+
force_fresh: tool10.schema.boolean().optional().default(false),
|
|
1860
|
+
max_concurrency: tool10.schema.number().optional().default(3)
|
|
2360
1861
|
},
|
|
2361
1862
|
async execute(args, context) {
|
|
2362
1863
|
const agents = args.agents || ["architect", "reviewer", "backend-coder"];
|
|
2363
1864
|
const concurrencyLimit = Math.max(1, Math.min(5, typeof args.max_concurrency === "number" ? args.max_concurrency : 3));
|
|
2364
1865
|
const index = readCodebaseIndex(context.directory);
|
|
2365
1866
|
const sp = statePath(context.directory);
|
|
2366
|
-
const rawState =
|
|
1867
|
+
const rawState = existsSync15(sp) ? readFileSync16(sp, "utf-8") : "";
|
|
2367
1868
|
const state = rawState ? parseState(rawState) : {};
|
|
2368
1869
|
const stateVersion = typeof state.summaryVersion === "number" ? state.summaryVersion : 0;
|
|
2369
1870
|
const indexVersion = typeof index.summaryVersion === "number" ? index.summaryVersion : 0;
|
|
@@ -2439,117 +1940,18 @@ Please synthesize these results. Identify areas of agreement, resolve conflicts,
|
|
|
2439
1940
|
function persistCouncilResult(directory, payload) {
|
|
2440
1941
|
try {
|
|
2441
1942
|
const base = codebaseDir(directory);
|
|
2442
|
-
if (!
|
|
2443
|
-
|
|
2444
|
-
const path =
|
|
1943
|
+
if (!existsSync15(base))
|
|
1944
|
+
mkdirSync10(base, { recursive: true });
|
|
1945
|
+
const path = join14(base, "COUNCILS.jsonl");
|
|
2445
1946
|
appendFileSync3(path, JSON.stringify(payload) + `
|
|
2446
1947
|
`, "utf-8");
|
|
2447
1948
|
} catch {}
|
|
2448
1949
|
}
|
|
2449
1950
|
|
|
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
1951
|
// src/tools/reflect.ts
|
|
2550
|
-
import { tool as
|
|
2551
|
-
import { existsSync as
|
|
2552
|
-
import { join as
|
|
1952
|
+
import { tool as tool11 } from "@opencode-ai/plugin";
|
|
1953
|
+
import { existsSync as existsSync16, readFileSync as readFileSync17 } from "fs";
|
|
1954
|
+
import { join as join15 } from "path";
|
|
2553
1955
|
var MAX_ARTIFACT_BYTES = 4000;
|
|
2554
1956
|
function tail(text, maxBytes) {
|
|
2555
1957
|
if (text.length <= maxBytes)
|
|
@@ -2557,10 +1959,10 @@ function tail(text, maxBytes) {
|
|
|
2557
1959
|
return `... (truncated) ...
|
|
2558
1960
|
` + text.slice(-maxBytes);
|
|
2559
1961
|
}
|
|
2560
|
-
var reflectTool =
|
|
1962
|
+
var reflectTool = tool11({
|
|
2561
1963
|
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
1964
|
args: {
|
|
2563
|
-
scope:
|
|
1965
|
+
scope: tool11.schema.enum(["session", "project"]).optional().describe("'session' (default) uses only recent artifacts; 'project' includes all historical data")
|
|
2564
1966
|
},
|
|
2565
1967
|
async execute(args, context) {
|
|
2566
1968
|
const root = context.directory;
|
|
@@ -2578,11 +1980,11 @@ var reflectTool = tool15({
|
|
|
2578
1980
|
];
|
|
2579
1981
|
let found = 0;
|
|
2580
1982
|
for (const [rel, label] of ARTIFACT_PATHS) {
|
|
2581
|
-
const full =
|
|
2582
|
-
if (!
|
|
1983
|
+
const full = join15(root, rel);
|
|
1984
|
+
if (!existsSync16(full))
|
|
2583
1985
|
continue;
|
|
2584
1986
|
try {
|
|
2585
|
-
const raw =
|
|
1987
|
+
const raw = readFileSync17(full, "utf-8").trim();
|
|
2586
1988
|
if (!raw)
|
|
2587
1989
|
continue;
|
|
2588
1990
|
const count = raw.split(`
|
|
@@ -2595,23 +1997,23 @@ var reflectTool = tool15({
|
|
|
2595
1997
|
return `No FlowDeck artifacts found under .codebase/.
|
|
2596
1998
|
` + "Run some tasks first so decisions, telemetry, and failures are recorded.";
|
|
2597
1999
|
}
|
|
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:", " -
|
|
2000
|
+
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
2001
|
return sections.join(`
|
|
2600
2002
|
`);
|
|
2601
2003
|
}
|
|
2602
2004
|
});
|
|
2603
2005
|
|
|
2604
2006
|
// src/tools/codegraph-tool.ts
|
|
2605
|
-
import { tool as
|
|
2007
|
+
import { tool as tool12 } from "@opencode-ai/plugin";
|
|
2606
2008
|
|
|
2607
2009
|
// src/services/codegraph.ts
|
|
2608
2010
|
import { spawnSync } from "child_process";
|
|
2609
|
-
import { existsSync as
|
|
2610
|
-
import { join as
|
|
2011
|
+
import { existsSync as existsSync17, readFileSync as readFileSync18, writeFileSync as writeFileSync12, mkdirSync as mkdirSync11 } from "fs";
|
|
2012
|
+
import { join as join16 } from "path";
|
|
2611
2013
|
var CODEGRAPH_META_FILE = "CODEGRAPH.md";
|
|
2612
2014
|
var MAX_FRESHNESS_MS = 30 * 60 * 1000;
|
|
2613
2015
|
function metaPath(dir) {
|
|
2614
|
-
return
|
|
2016
|
+
return join16(codebaseDir(dir), CODEGRAPH_META_FILE);
|
|
2615
2017
|
}
|
|
2616
2018
|
function isCodegraphInstalled() {
|
|
2617
2019
|
try {
|
|
@@ -2626,11 +2028,11 @@ function isCodegraphInstalled() {
|
|
|
2626
2028
|
}
|
|
2627
2029
|
}
|
|
2628
2030
|
function isCodegraphIndexed(dir) {
|
|
2629
|
-
return
|
|
2031
|
+
return existsSync17(join16(dir, ".codegraph", "codegraph.db"));
|
|
2630
2032
|
}
|
|
2631
2033
|
function readCodegraphMeta(dir) {
|
|
2632
2034
|
const path = metaPath(dir);
|
|
2633
|
-
if (!
|
|
2035
|
+
if (!existsSync17(path)) {
|
|
2634
2036
|
return {
|
|
2635
2037
|
installed: false,
|
|
2636
2038
|
indexed: false,
|
|
@@ -2643,7 +2045,7 @@ function readCodegraphMeta(dir) {
|
|
|
2643
2045
|
};
|
|
2644
2046
|
}
|
|
2645
2047
|
try {
|
|
2646
|
-
const content =
|
|
2048
|
+
const content = readFileSync18(path, "utf-8");
|
|
2647
2049
|
return parseCodegraphMeta(content);
|
|
2648
2050
|
} catch {
|
|
2649
2051
|
return {
|
|
@@ -2710,8 +2112,8 @@ function parseCodegraphMeta(content) {
|
|
|
2710
2112
|
}
|
|
2711
2113
|
function writeCodegraphMeta(dir, meta) {
|
|
2712
2114
|
const base = codebaseDir(dir);
|
|
2713
|
-
if (!
|
|
2714
|
-
|
|
2115
|
+
if (!existsSync17(base))
|
|
2116
|
+
mkdirSync11(base, { recursive: true });
|
|
2715
2117
|
const lines = [
|
|
2716
2118
|
"# Codegraph Metadata",
|
|
2717
2119
|
"",
|
|
@@ -2724,7 +2126,7 @@ function writeCodegraphMeta(dir, meta) {
|
|
|
2724
2126
|
`**installLog:** ${meta.installLog}`,
|
|
2725
2127
|
`**indexLog:** ${meta.indexLog}`
|
|
2726
2128
|
];
|
|
2727
|
-
|
|
2129
|
+
writeFileSync12(metaPath(dir), lines.join(`
|
|
2728
2130
|
`), "utf-8");
|
|
2729
2131
|
}
|
|
2730
2132
|
function isCodegraphFresh(dir, maxAgeMs = MAX_FRESHNESS_MS) {
|
|
@@ -2935,11 +2337,11 @@ function markCodegraphStale(dir) {
|
|
|
2935
2337
|
}
|
|
2936
2338
|
|
|
2937
2339
|
// src/tools/codegraph-tool.ts
|
|
2938
|
-
var codegraphTool =
|
|
2340
|
+
var codegraphTool = tool12({
|
|
2939
2341
|
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
2342
|
args: {
|
|
2941
|
-
action:
|
|
2942
|
-
agent:
|
|
2343
|
+
action: tool12.schema.enum(["check", "install", "init", "refresh", "status", "mark-stale"]),
|
|
2344
|
+
agent: tool12.schema.string().optional()
|
|
2943
2345
|
},
|
|
2944
2346
|
async execute(args, context) {
|
|
2945
2347
|
const dir = context.directory ?? process.cwd();
|
|
@@ -3028,21 +2430,21 @@ var codegraphTool = tool16({
|
|
|
3028
2430
|
});
|
|
3029
2431
|
|
|
3030
2432
|
// src/tools/load-rules.ts
|
|
3031
|
-
import { tool as
|
|
3032
|
-
import { existsSync as
|
|
3033
|
-
import { join as
|
|
3034
|
-
import { fileURLToPath
|
|
3035
|
-
var RULES_DIR =
|
|
2433
|
+
import { tool as tool13 } from "@opencode-ai/plugin";
|
|
2434
|
+
import { existsSync as existsSync18, readFileSync as readFileSync19 } from "fs";
|
|
2435
|
+
import { join as join17, dirname as dirname2 } from "path";
|
|
2436
|
+
import { fileURLToPath } from "url";
|
|
2437
|
+
var RULES_DIR = join17(dirname2(fileURLToPath(import.meta.url)), "..", "rules");
|
|
3036
2438
|
var _loadedPaths = new Set;
|
|
3037
|
-
var loadRulesTool =
|
|
2439
|
+
var loadRulesTool = tool13({
|
|
3038
2440
|
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
2441
|
args: {
|
|
3040
|
-
stage:
|
|
3041
|
-
languages:
|
|
3042
|
-
force_reload:
|
|
2442
|
+
stage: tool13.schema.string().optional().describe("Current workflow stage: discuss | plan | execute | verify | fix-bug | write-docs"),
|
|
2443
|
+
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)."),
|
|
2444
|
+
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
2445
|
},
|
|
3044
2446
|
async execute(args) {
|
|
3045
|
-
const rulesDir =
|
|
2447
|
+
const rulesDir = existsSync18(RULES_DIR) ? RULES_DIR : null;
|
|
3046
2448
|
if (!rulesDir) {
|
|
3047
2449
|
return JSON.stringify({
|
|
3048
2450
|
loaded: [],
|
|
@@ -3068,7 +2470,7 @@ var loadRulesTool = tool17({
|
|
|
3068
2470
|
continue;
|
|
3069
2471
|
}
|
|
3070
2472
|
try {
|
|
3071
|
-
const text =
|
|
2473
|
+
const text = readFileSync19(rule.path, "utf-8");
|
|
3072
2474
|
contents.push(`## ${name}
|
|
3073
2475
|
|
|
3074
2476
|
${text}`);
|
|
@@ -3099,11 +2501,11 @@ ${text}`);
|
|
|
3099
2501
|
function ruleShortName(rule) {
|
|
3100
2502
|
return rule.path.replace(RULES_DIR + "/", "").replace(/\.md$/, "");
|
|
3101
2503
|
}
|
|
3102
|
-
var listRulesTool =
|
|
2504
|
+
var listRulesTool = tool13({
|
|
3103
2505
|
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
2506
|
args: {},
|
|
3105
2507
|
async execute() {
|
|
3106
|
-
const rulesDir =
|
|
2508
|
+
const rulesDir = existsSync18(RULES_DIR) ? RULES_DIR : null;
|
|
3107
2509
|
if (!rulesDir) {
|
|
3108
2510
|
return JSON.stringify({ rules: [], error: `Rules directory not found at ${RULES_DIR}` });
|
|
3109
2511
|
}
|
|
@@ -3123,13 +2525,13 @@ var listRulesTool = tool17({
|
|
|
3123
2525
|
});
|
|
3124
2526
|
|
|
3125
2527
|
// src/tools/rtk-setup.ts
|
|
3126
|
-
import { tool as
|
|
2528
|
+
import { tool as tool14 } from "@opencode-ai/plugin";
|
|
3127
2529
|
|
|
3128
2530
|
// src/services/rtk-manager.ts
|
|
3129
2531
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
3130
|
-
import { existsSync as
|
|
2532
|
+
import { existsSync as existsSync19 } from "fs";
|
|
3131
2533
|
import { homedir as homedir2 } from "os";
|
|
3132
|
-
import { join as
|
|
2534
|
+
import { join as join18 } from "path";
|
|
3133
2535
|
|
|
3134
2536
|
// src/services/rtk-policy.ts
|
|
3135
2537
|
var SUPPORTED_COMMANDS = new Set([
|
|
@@ -3175,7 +2577,7 @@ var INSTALL_INSTRUCTIONS = [
|
|
|
3175
2577
|
"After installation, call rtk-setup again to verify detection."
|
|
3176
2578
|
].join(`
|
|
3177
2579
|
`);
|
|
3178
|
-
var CANDIDATE_PATHS = [
|
|
2580
|
+
var CANDIDATE_PATHS = [join18(homedir2(), ".local", "bin", "rtk"), "/usr/local/bin/rtk", "/usr/bin/rtk"];
|
|
3179
2581
|
function detectRtk() {
|
|
3180
2582
|
const fromPath = spawnSync2("rtk", ["--version"], { encoding: "utf-8", timeout: 5000 });
|
|
3181
2583
|
if (fromPath.status === 0) {
|
|
@@ -3184,7 +2586,7 @@ function detectRtk() {
|
|
|
3184
2586
|
return { installed: true, binPath: "rtk", version };
|
|
3185
2587
|
}
|
|
3186
2588
|
for (const candidate of CANDIDATE_PATHS) {
|
|
3187
|
-
if (!
|
|
2589
|
+
if (!existsSync19(candidate))
|
|
3188
2590
|
continue;
|
|
3189
2591
|
const result = spawnSync2(candidate, ["--version"], { encoding: "utf-8", timeout: 5000 });
|
|
3190
2592
|
if (result.status === 0) {
|
|
@@ -3262,7 +2664,7 @@ function getRtkStatus(opts) {
|
|
|
3262
2664
|
}
|
|
3263
2665
|
|
|
3264
2666
|
// src/tools/rtk-setup.ts
|
|
3265
|
-
var rtkSetupTool =
|
|
2667
|
+
var rtkSetupTool = tool14({
|
|
3266
2668
|
description: [
|
|
3267
2669
|
"Detect, initialize, and report status of rtk (output compression proxy for CLI commands).",
|
|
3268
2670
|
"rtk reduces noisy CLI output (git, npm, test runners, linters, docker) by 60-90%.",
|
|
@@ -3270,7 +2672,7 @@ var rtkSetupTool = tool18({
|
|
|
3270
2672
|
"When RTK_INSTALLED=true in the environment, use `$RTK_BIN git status` for compressed output."
|
|
3271
2673
|
].join(" "),
|
|
3272
2674
|
args: {
|
|
3273
|
-
action:
|
|
2675
|
+
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
2676
|
},
|
|
3275
2677
|
async execute(args) {
|
|
3276
2678
|
const action = args.action ?? "status";
|
|
@@ -3310,15 +2712,15 @@ var rtkSetupTool = tool18({
|
|
|
3310
2712
|
});
|
|
3311
2713
|
|
|
3312
2714
|
// src/hooks/guard-rails.ts
|
|
3313
|
-
import { existsSync as
|
|
3314
|
-
import { join as
|
|
2715
|
+
import { existsSync as existsSync20, readFileSync as readFileSync20 } from "fs";
|
|
2716
|
+
import { join as join19 } from "path";
|
|
3315
2717
|
var PLANNING_DIR2 = ".planning";
|
|
3316
2718
|
var CONFIG_FILE = "config.json";
|
|
3317
2719
|
var STATE_FILE2 = "STATE.md";
|
|
3318
2720
|
function resolveExecutionMode(configPath, trustScore, volatility) {
|
|
3319
|
-
if (
|
|
2721
|
+
if (existsSync20(configPath)) {
|
|
3320
2722
|
try {
|
|
3321
|
-
const config = JSON.parse(
|
|
2723
|
+
const config = JSON.parse(readFileSync20(configPath, "utf-8"));
|
|
3322
2724
|
if (config.execution_mode === "review-only")
|
|
3323
2725
|
return "review-only";
|
|
3324
2726
|
if (config.execution_mode === "guarded")
|
|
@@ -3372,22 +2774,22 @@ async function guardRailsHook(ctx, input, _output) {
|
|
|
3372
2774
|
if (!ENABLED)
|
|
3373
2775
|
return;
|
|
3374
2776
|
const dir = ctx.directory;
|
|
3375
|
-
const planningDirPath =
|
|
2777
|
+
const planningDirPath = join19(dir, PLANNING_DIR2);
|
|
3376
2778
|
const codebaseDirectory = codebaseDir(dir);
|
|
3377
|
-
const configPath =
|
|
3378
|
-
const statePath2 =
|
|
2779
|
+
const configPath = join19(planningDirPath, CONFIG_FILE);
|
|
2780
|
+
const statePath2 = join19(planningDirPath, STATE_FILE2);
|
|
3379
2781
|
const workspaceRoot = findWorkspaceRoot(dir);
|
|
3380
2782
|
if (workspaceRoot && dir !== workspaceRoot) {
|
|
3381
2783
|
const config = getWorkspaceConfig(dir);
|
|
3382
|
-
if (config && config.workspace_mode === "shared" && !
|
|
2784
|
+
if (config && config.workspace_mode === "shared" && !existsSync20(planningDirPath)) {
|
|
3383
2785
|
const msg = `No .planning/ in this sub-repo. Switch to workspace root: cd ${workspaceRoot}`;
|
|
3384
2786
|
throw new Error(`[flowdeck] BLOCK: ${msg}`);
|
|
3385
2787
|
}
|
|
3386
2788
|
}
|
|
3387
2789
|
if (input.tool === "write" || input.tool === "edit") {
|
|
3388
|
-
if (!
|
|
2790
|
+
if (!existsSync20(planningDirPath))
|
|
3389
2791
|
return;
|
|
3390
|
-
if (!
|
|
2792
|
+
if (!existsSync20(codebaseDirectory)) {
|
|
3391
2793
|
throw new Error(`[flowdeck] WARNING: .codebase/ not found. Run /fd-map-codebase to map the codebase.`);
|
|
3392
2794
|
}
|
|
3393
2795
|
const execMode = resolveExecutionMode(configPath, null);
|
|
@@ -3443,15 +2845,15 @@ function getDesignGateMessage(dir) {
|
|
|
3443
2845
|
}
|
|
3444
2846
|
function planSuggestsUiHeavy(dir, phase) {
|
|
3445
2847
|
const planPath = phasePlanPath(dir, phase);
|
|
3446
|
-
if (!
|
|
2848
|
+
if (!existsSync20(planPath))
|
|
3447
2849
|
return false;
|
|
3448
|
-
const planContent =
|
|
2850
|
+
const planContent = readFileSync20(planPath, "utf-8");
|
|
3449
2851
|
return isUiHeavyTask(planContent);
|
|
3450
2852
|
}
|
|
3451
2853
|
function effectiveSeverity(configPath, statePath2) {
|
|
3452
|
-
if (
|
|
2854
|
+
if (existsSync20(configPath)) {
|
|
3453
2855
|
try {
|
|
3454
|
-
const configContent =
|
|
2856
|
+
const configContent = readFileSync20(configPath, "utf-8");
|
|
3455
2857
|
const config = JSON.parse(configContent);
|
|
3456
2858
|
if (config.guard_enforcement === "warn")
|
|
3457
2859
|
return "warn";
|
|
@@ -3467,10 +2869,10 @@ function getEffectiveSeverity(configPath, statePath2) {
|
|
|
3467
2869
|
return effectiveSeverity(configPath, statePath2);
|
|
3468
2870
|
}
|
|
3469
2871
|
function getPlanConfirmed(statePath2) {
|
|
3470
|
-
if (!
|
|
2872
|
+
if (!existsSync20(statePath2))
|
|
3471
2873
|
return false;
|
|
3472
2874
|
try {
|
|
3473
|
-
const content =
|
|
2875
|
+
const content = readFileSync20(statePath2, "utf-8");
|
|
3474
2876
|
const match = content.match(/plan_confirmed:\s*(true|false)/i);
|
|
3475
2877
|
return match ? match[1].toLowerCase() === "true" : false;
|
|
3476
2878
|
} catch {
|
|
@@ -3478,32 +2880,32 @@ function getPlanConfirmed(statePath2) {
|
|
|
3478
2880
|
}
|
|
3479
2881
|
}
|
|
3480
2882
|
function getWarningMessage(planningDir2) {
|
|
3481
|
-
if (!
|
|
2883
|
+
if (!existsSync20(join19(planningDir2, STATE_FILE2))) {
|
|
3482
2884
|
return "No STATE.md found. Run /fd-map-codebase then /fd-new-feature to start a feature.";
|
|
3483
2885
|
}
|
|
3484
2886
|
return "Plan not confirmed. Run /fd-plan and confirm to enable execution.";
|
|
3485
2887
|
}
|
|
3486
2888
|
function getBlockMessage(planningDir2) {
|
|
3487
|
-
if (!
|
|
2889
|
+
if (!existsSync20(join19(planningDir2, STATE_FILE2))) {
|
|
3488
2890
|
return "No STATE.md found. Run /fd-map-codebase then /fd-new-feature to start a feature.";
|
|
3489
2891
|
}
|
|
3490
2892
|
return "Plan not confirmed. Run /fd-plan and confirm to enable execution.";
|
|
3491
2893
|
}
|
|
3492
2894
|
|
|
3493
2895
|
// src/hooks/tool-guard.ts
|
|
3494
|
-
import { existsSync as
|
|
3495
|
-
import { join as
|
|
2896
|
+
import { existsSync as existsSync21, readFileSync as readFileSync21 } from "fs";
|
|
2897
|
+
import { join as join20 } from "path";
|
|
3496
2898
|
var IS_ENABLED = () => process.env.FLOWDECK_TOOL_GUARD_ENABLED === "on";
|
|
3497
2899
|
var BLOCKED_PATTERNS = {
|
|
3498
2900
|
read: [".env", ".pem", ".key", ".secret"],
|
|
3499
2901
|
write: ["node_modules"],
|
|
3500
2902
|
bash: ["rm -rf"]
|
|
3501
2903
|
};
|
|
3502
|
-
function isBlocked(
|
|
3503
|
-
const patterns = BLOCKED_PATTERNS[
|
|
2904
|
+
function isBlocked(tool15, args) {
|
|
2905
|
+
const patterns = BLOCKED_PATTERNS[tool15];
|
|
3504
2906
|
if (!patterns)
|
|
3505
2907
|
return null;
|
|
3506
|
-
if (
|
|
2908
|
+
if (tool15 === "bash") {
|
|
3507
2909
|
const cmd = args.command;
|
|
3508
2910
|
if (!cmd)
|
|
3509
2911
|
return null;
|
|
@@ -3514,7 +2916,7 @@ function isBlocked(tool19, args) {
|
|
|
3514
2916
|
}
|
|
3515
2917
|
return null;
|
|
3516
2918
|
}
|
|
3517
|
-
if (
|
|
2919
|
+
if (tool15 === "read") {
|
|
3518
2920
|
const filePath = args.filePath;
|
|
3519
2921
|
if (!filePath)
|
|
3520
2922
|
return null;
|
|
@@ -3525,7 +2927,7 @@ function isBlocked(tool19, args) {
|
|
|
3525
2927
|
}
|
|
3526
2928
|
return null;
|
|
3527
2929
|
}
|
|
3528
|
-
if (
|
|
2930
|
+
if (tool15 === "write") {
|
|
3529
2931
|
const filePath = args.filePath;
|
|
3530
2932
|
if (!filePath)
|
|
3531
2933
|
return null;
|
|
@@ -3539,11 +2941,11 @@ function isBlocked(tool19, args) {
|
|
|
3539
2941
|
return null;
|
|
3540
2942
|
}
|
|
3541
2943
|
function checkArchConstraint(directory, filePath) {
|
|
3542
|
-
const constraintsPath =
|
|
3543
|
-
if (!
|
|
2944
|
+
const constraintsPath = join20(codebaseDir(directory), "CONSTRAINTS.md");
|
|
2945
|
+
if (!existsSync21(constraintsPath))
|
|
3544
2946
|
return null;
|
|
3545
2947
|
try {
|
|
3546
|
-
const content =
|
|
2948
|
+
const content = readFileSync21(constraintsPath, "utf-8");
|
|
3547
2949
|
const match = content.match(/## Forbidden Paths\n([\s\S]*?)(?:\n##|$)/);
|
|
3548
2950
|
if (!match)
|
|
3549
2951
|
return null;
|
|
@@ -3584,9 +2986,9 @@ function isUiDesignApprovalRequired(directory) {
|
|
|
3584
2986
|
return !(state.design_stage === "handoff_complete" && state.design_approved);
|
|
3585
2987
|
}
|
|
3586
2988
|
const planPath = phasePlanPath(directory, state.phase || 1);
|
|
3587
|
-
if (!
|
|
2989
|
+
if (!existsSync21(planPath))
|
|
3588
2990
|
return false;
|
|
3589
|
-
const planContent =
|
|
2991
|
+
const planContent = readFileSync21(planPath, "utf-8");
|
|
3590
2992
|
if (!isUiHeavyTask(planContent))
|
|
3591
2993
|
return false;
|
|
3592
2994
|
return !(state.design_stage === "handoff_complete" && state.design_approved);
|
|
@@ -3615,18 +3017,18 @@ async function toolGuardHook(ctx, input, output) {
|
|
|
3615
3017
|
}
|
|
3616
3018
|
|
|
3617
3019
|
// src/hooks/session-start.ts
|
|
3618
|
-
import { existsSync as
|
|
3020
|
+
import { existsSync as existsSync22, readFileSync as readFileSync22 } from "fs";
|
|
3619
3021
|
async function sessionStartHook(ctx) {
|
|
3620
3022
|
const planningDir2 = ctx.directory + "/.planning";
|
|
3621
3023
|
const codebaseDirectory = codebaseDir(ctx.directory);
|
|
3622
3024
|
const workspaceRoot = findWorkspaceRoot(ctx.directory);
|
|
3623
3025
|
const config = workspaceRoot ? getWorkspaceConfig(ctx.directory) : null;
|
|
3624
|
-
if (!
|
|
3026
|
+
if (!existsSync22(planningDir2)) {
|
|
3625
3027
|
return {
|
|
3626
3028
|
flowdeck_phase: null,
|
|
3627
3029
|
flowdeck_status: "no_plan",
|
|
3628
3030
|
flowdeck_warning: "Run /fd-map-codebase to index the codebase, then /fd-new-feature to start a feature.",
|
|
3629
|
-
flowdeck_has_codebase:
|
|
3031
|
+
flowdeck_has_codebase: existsSync22(codebaseDirectory),
|
|
3630
3032
|
...workspaceRoot && config?.sub_repos ? {
|
|
3631
3033
|
flowdeck_workspace_root: workspaceRoot,
|
|
3632
3034
|
flowdeck_sub_repos: config.sub_repos,
|
|
@@ -3637,7 +3039,7 @@ async function sessionStartHook(ctx) {
|
|
|
3637
3039
|
}
|
|
3638
3040
|
try {
|
|
3639
3041
|
const stateFilePath = statePath(ctx.directory);
|
|
3640
|
-
const content =
|
|
3042
|
+
const content = readFileSync22(stateFilePath, "utf-8");
|
|
3641
3043
|
const state = parseState(content);
|
|
3642
3044
|
const currentPhase = state["current_phase"] || {};
|
|
3643
3045
|
const result = {
|
|
@@ -3645,7 +3047,7 @@ async function sessionStartHook(ctx) {
|
|
|
3645
3047
|
flowdeck_status: currentPhase["status"] ?? null,
|
|
3646
3048
|
flowdeck_steps_pending: currentPhase["steps_pending"] ?? null,
|
|
3647
3049
|
flowdeck_last_action: currentPhase["last_action"] ?? null,
|
|
3648
|
-
flowdeck_has_codebase:
|
|
3050
|
+
flowdeck_has_codebase: existsSync22(codebaseDirectory)
|
|
3649
3051
|
};
|
|
3650
3052
|
if (workspaceRoot && config?.sub_repos && config.sub_repos.length > 0) {
|
|
3651
3053
|
result.flowdeck_workspace_root = workspaceRoot;
|
|
@@ -3660,7 +3062,7 @@ async function sessionStartHook(ctx) {
|
|
|
3660
3062
|
flowdeck_phase: null,
|
|
3661
3063
|
flowdeck_status: "error",
|
|
3662
3064
|
flowdeck_warning: "State file unreadable. Continuing without flowdeck context.",
|
|
3663
|
-
flowdeck_has_codebase:
|
|
3065
|
+
flowdeck_has_codebase: existsSync22(codebaseDirectory)
|
|
3664
3066
|
};
|
|
3665
3067
|
if (workspaceRoot && config?.sub_repos && config.sub_repos.length > 0) {
|
|
3666
3068
|
result.flowdeck_workspace_root = workspaceRoot;
|
|
@@ -3690,7 +3092,7 @@ var COMPLETION_COMMANDS = new Set([
|
|
|
3690
3092
|
"execute",
|
|
3691
3093
|
"verify"
|
|
3692
3094
|
]);
|
|
3693
|
-
function
|
|
3095
|
+
function normalizeCommandName(raw) {
|
|
3694
3096
|
return raw.replace(/^\//, "").replace(/^fd-/, "");
|
|
3695
3097
|
}
|
|
3696
3098
|
function notify(title, body, level = "info") {
|
|
@@ -3735,7 +3137,7 @@ class NotificationController {
|
|
|
3735
3137
|
this.log = log;
|
|
3736
3138
|
}
|
|
3737
3139
|
onCommandExecuted(rawCommand) {
|
|
3738
|
-
const name =
|
|
3140
|
+
const name = normalizeCommandName(rawCommand);
|
|
3739
3141
|
if (!INTERACTIVE_COMMANDS.has(name) && !COMPLETION_COMMANDS.has(name)) {
|
|
3740
3142
|
this.log(`[notify] command.executed: "${name}" — not a tracked command, skipping`);
|
|
3741
3143
|
return;
|
|
@@ -3799,13 +3201,13 @@ class NotificationController {
|
|
|
3799
3201
|
return this.lastNotifiedKey;
|
|
3800
3202
|
}
|
|
3801
3203
|
}
|
|
3802
|
-
function notifyPermissionNeeded(
|
|
3803
|
-
notify("FlowDeck Permission Required", `Agent needs approval to use tool: ${
|
|
3204
|
+
function notifyPermissionNeeded(tool15) {
|
|
3205
|
+
notify("FlowDeck Permission Required", `Agent needs approval to use tool: ${tool15}`, "critical");
|
|
3804
3206
|
}
|
|
3805
3207
|
|
|
3806
3208
|
// src/hooks/patch-trust.ts
|
|
3807
|
-
import { existsSync as
|
|
3808
|
-
import { join as
|
|
3209
|
+
import { existsSync as existsSync23, readFileSync as readFileSync23 } from "fs";
|
|
3210
|
+
import { join as join21 } from "path";
|
|
3809
3211
|
var HIGH_RISK_KEYWORDS = [
|
|
3810
3212
|
"password",
|
|
3811
3213
|
"secret",
|
|
@@ -3826,26 +3228,12 @@ var HIGH_RISK_KEYWORDS = [
|
|
|
3826
3228
|
"root",
|
|
3827
3229
|
"privilege"
|
|
3828
3230
|
];
|
|
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
3231
|
function loadFailedPaths(directory) {
|
|
3844
|
-
const p =
|
|
3845
|
-
if (!
|
|
3232
|
+
const p = join21(codebaseDir(directory), "FAILURES.json");
|
|
3233
|
+
if (!existsSync23(p))
|
|
3846
3234
|
return [];
|
|
3847
3235
|
try {
|
|
3848
|
-
const data = JSON.parse(
|
|
3236
|
+
const data = JSON.parse(readFileSync23(p, "utf-8"));
|
|
3849
3237
|
return (data.entries ?? []).flatMap((e) => e.affected_paths ?? []);
|
|
3850
3238
|
} catch {
|
|
3851
3239
|
return [];
|
|
@@ -3854,18 +3242,6 @@ function loadFailedPaths(directory) {
|
|
|
3854
3242
|
function scorePatch(directory, filePath, content) {
|
|
3855
3243
|
let score = 100;
|
|
3856
3244
|
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
3245
|
const failedPaths = loadFailedPaths(directory);
|
|
3870
3246
|
if (failedPaths.some((p) => filePath.includes(p))) {
|
|
3871
3247
|
score -= 20;
|
|
@@ -3910,8 +3286,8 @@ async function patchTrustHook(ctx, input, output) {
|
|
|
3910
3286
|
}
|
|
3911
3287
|
|
|
3912
3288
|
// src/hooks/decision-trace-hook.ts
|
|
3913
|
-
import { existsSync as
|
|
3914
|
-
import { join as
|
|
3289
|
+
import { existsSync as existsSync24, mkdirSync as mkdirSync12, appendFileSync as appendFileSync4 } from "fs";
|
|
3290
|
+
import { join as join22 } from "path";
|
|
3915
3291
|
async function decisionTraceHook(ctx, input, output) {
|
|
3916
3292
|
if (input.tool !== "write" && input.tool !== "edit")
|
|
3917
3293
|
return;
|
|
@@ -3920,8 +3296,8 @@ async function decisionTraceHook(ctx, input, output) {
|
|
|
3920
3296
|
return;
|
|
3921
3297
|
const base = codebaseDir(ctx.directory);
|
|
3922
3298
|
try {
|
|
3923
|
-
if (!
|
|
3924
|
-
|
|
3299
|
+
if (!existsSync24(base))
|
|
3300
|
+
mkdirSync12(base, { recursive: true });
|
|
3925
3301
|
const entry = {
|
|
3926
3302
|
timestamp: new Date().toISOString(),
|
|
3927
3303
|
file_path: filePath,
|
|
@@ -3933,164 +3309,14 @@ async function decisionTraceHook(ctx, input, output) {
|
|
|
3933
3309
|
risk_level: "unknown",
|
|
3934
3310
|
auto_recorded: true
|
|
3935
3311
|
};
|
|
3936
|
-
appendFileSync4(
|
|
3312
|
+
appendFileSync4(join22(base, "DECISIONS.jsonl"), JSON.stringify(entry) + `
|
|
3937
3313
|
`, "utf-8");
|
|
3938
3314
|
} catch {}
|
|
3939
3315
|
}
|
|
3940
3316
|
|
|
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
3317
|
// src/services/approval-manager.ts
|
|
4092
|
-
import { existsSync as
|
|
4093
|
-
import { join as
|
|
3318
|
+
import { existsSync as existsSync25, readFileSync as readFileSync24, writeFileSync as writeFileSync13, mkdirSync as mkdirSync13 } from "fs";
|
|
3319
|
+
import { join as join23 } from "path";
|
|
4094
3320
|
var APPROVAL_TTL_MS = 30 * 60 * 1000;
|
|
4095
3321
|
var SENSITIVE_PATTERNS = [
|
|
4096
3322
|
/auth/i,
|
|
@@ -4127,14 +3353,14 @@ function isSensitivePath(filePath) {
|
|
|
4127
3353
|
return SENSITIVE_PATTERNS.some((p) => p.test(filePath));
|
|
4128
3354
|
}
|
|
4129
3355
|
function approvalsPath(dir) {
|
|
4130
|
-
return
|
|
3356
|
+
return join23(codebaseDir(dir), "APPROVALS.json");
|
|
4131
3357
|
}
|
|
4132
3358
|
function loadStore2(dir) {
|
|
4133
3359
|
const p = approvalsPath(dir);
|
|
4134
|
-
if (!
|
|
3360
|
+
if (!existsSync25(p))
|
|
4135
3361
|
return { requests: [] };
|
|
4136
3362
|
try {
|
|
4137
|
-
return JSON.parse(
|
|
3363
|
+
return JSON.parse(readFileSync24(p, "utf-8"));
|
|
4138
3364
|
} catch {
|
|
4139
3365
|
return { requests: [] };
|
|
4140
3366
|
}
|
|
@@ -4152,8 +3378,8 @@ async function approvalHook(context, toolInput, output) {
|
|
|
4152
3378
|
if (!ENABLED2)
|
|
4153
3379
|
return;
|
|
4154
3380
|
const dir = context.directory ?? process.cwd();
|
|
4155
|
-
const
|
|
4156
|
-
if (!WRITE_TOOLS.has(
|
|
3381
|
+
const tool15 = toolInput.name ?? toolInput.tool ?? "";
|
|
3382
|
+
if (!WRITE_TOOLS.has(tool15))
|
|
4157
3383
|
return;
|
|
4158
3384
|
const args = output.args ?? {};
|
|
4159
3385
|
const filePath = String(args.path ?? args.file_path ?? args.filename ?? "");
|
|
@@ -4164,20 +3390,305 @@ async function approvalHook(context, toolInput, output) {
|
|
|
4164
3390
|
const approval = checkApproval(dir, filePath, "");
|
|
4165
3391
|
if (approval)
|
|
4166
3392
|
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
3393
|
throw new Error(`APPROVAL_REQUIRED: "${filePath}" is a sensitive file (auth/payment/secrets/infra).
|
|
4177
3394
|
` + `Risk level: HIGH — manual approval needed before editing.
|
|
4178
3395
|
` + `To proceed: run /fd-guarded-edit --file "${filePath}" to review and approve this change.`);
|
|
4179
3396
|
}
|
|
4180
3397
|
|
|
3398
|
+
// src/services/event-logger.ts
|
|
3399
|
+
import { existsSync as existsSync26, mkdirSync as mkdirSync14, appendFileSync as appendFileSync5, readFileSync as readFileSync25, writeFileSync as writeFileSync14, renameSync, unlinkSync, statSync as statSync2 } from "fs";
|
|
3400
|
+
import { join as join24, resolve as resolve2, sep } from "path";
|
|
3401
|
+
var SENSITIVE_KEYS = [
|
|
3402
|
+
"password",
|
|
3403
|
+
"token",
|
|
3404
|
+
"apikey",
|
|
3405
|
+
"api_key",
|
|
3406
|
+
"secret",
|
|
3407
|
+
"authorization",
|
|
3408
|
+
"auth",
|
|
3409
|
+
"key",
|
|
3410
|
+
"credential",
|
|
3411
|
+
"privatekey",
|
|
3412
|
+
"private_key",
|
|
3413
|
+
"accesstoken",
|
|
3414
|
+
"access_token",
|
|
3415
|
+
"refreshtoken",
|
|
3416
|
+
"refresh_token"
|
|
3417
|
+
];
|
|
3418
|
+
var currentAgent = null;
|
|
3419
|
+
function getCurrentAgent() {
|
|
3420
|
+
return currentAgent;
|
|
3421
|
+
}
|
|
3422
|
+
function setCurrentAgent(agent) {
|
|
3423
|
+
currentAgent = agent;
|
|
3424
|
+
}
|
|
3425
|
+
function sanitizeArgs(args) {
|
|
3426
|
+
if (!args || typeof args !== "object")
|
|
3427
|
+
return {};
|
|
3428
|
+
const result = {};
|
|
3429
|
+
for (const [key, value] of Object.entries(args)) {
|
|
3430
|
+
const lowerKey = key.toLowerCase();
|
|
3431
|
+
if (SENSITIVE_KEYS.some((sk) => lowerKey.includes(sk))) {
|
|
3432
|
+
result[key] = "[REDACTED]";
|
|
3433
|
+
} else if (key === "content" || key === "newString" || key === "oldString" || key === "template") {
|
|
3434
|
+
if (typeof value === "string" && value.length > 100) {
|
|
3435
|
+
result[key] = `[${value.length} chars truncated]`;
|
|
3436
|
+
} else {
|
|
3437
|
+
result[key] = value;
|
|
3438
|
+
}
|
|
3439
|
+
} else {
|
|
3440
|
+
result[key] = value;
|
|
3441
|
+
}
|
|
3442
|
+
}
|
|
3443
|
+
return result;
|
|
3444
|
+
}
|
|
3445
|
+
function isValidDirectory(directory) {
|
|
3446
|
+
const normalized = resolve2(directory);
|
|
3447
|
+
if (normalized !== directory && !directory.startsWith(sep)) {
|
|
3448
|
+
return false;
|
|
3449
|
+
}
|
|
3450
|
+
if (directory.includes("..") || directory.includes(".." + sep)) {
|
|
3451
|
+
return false;
|
|
3452
|
+
}
|
|
3453
|
+
try {
|
|
3454
|
+
const stats = statSync2(directory);
|
|
3455
|
+
return stats.isDirectory();
|
|
3456
|
+
} catch {
|
|
3457
|
+
return false;
|
|
3458
|
+
}
|
|
3459
|
+
}
|
|
3460
|
+
function logEvent(directory, event) {
|
|
3461
|
+
if (process.env.FLOWDECK_EVENT_LOG === "off")
|
|
3462
|
+
return;
|
|
3463
|
+
if (!isValidDirectory(directory)) {
|
|
3464
|
+
process.stderr.write(`[FlowDeck] Invalid log directory: ${directory}
|
|
3465
|
+
`);
|
|
3466
|
+
return;
|
|
3467
|
+
}
|
|
3468
|
+
const logDir = join24(directory, ".opencode");
|
|
3469
|
+
const logPath = join24(logDir, "flowdeck-events.jsonl");
|
|
3470
|
+
try {
|
|
3471
|
+
if (!existsSync26(logDir)) {
|
|
3472
|
+
mkdirSync14(logDir, { recursive: true });
|
|
3473
|
+
}
|
|
3474
|
+
appendFileSync5(logPath, JSON.stringify(event) + `
|
|
3475
|
+
`, "utf-8");
|
|
3476
|
+
rotateLogFile(logPath);
|
|
3477
|
+
const line = formatEventForStderr(event);
|
|
3478
|
+
process.stderr.write(line + `
|
|
3479
|
+
`);
|
|
3480
|
+
} catch {}
|
|
3481
|
+
}
|
|
3482
|
+
function rotateLogFile(logPath) {
|
|
3483
|
+
try {
|
|
3484
|
+
const stats = statSync2(logPath);
|
|
3485
|
+
if (stats.size < 5000)
|
|
3486
|
+
return;
|
|
3487
|
+
const content = readFileSync25(logPath, "utf-8");
|
|
3488
|
+
const lines = content.split(`
|
|
3489
|
+
`).filter((l) => l.trim());
|
|
3490
|
+
if (lines.length > 1000) {
|
|
3491
|
+
const backupPath = logPath + ".backup";
|
|
3492
|
+
renameSync(logPath, backupPath);
|
|
3493
|
+
const keep = lines.slice(-1000);
|
|
3494
|
+
writeFileSync14(logPath, keep.join(`
|
|
3495
|
+
`) + `
|
|
3496
|
+
`, "utf-8");
|
|
3497
|
+
try {
|
|
3498
|
+
unlinkSync(backupPath);
|
|
3499
|
+
} catch {}
|
|
3500
|
+
}
|
|
3501
|
+
} catch {}
|
|
3502
|
+
}
|
|
3503
|
+
function formatEventForStderr(event) {
|
|
3504
|
+
const time = event.timestamp.slice(11, 23);
|
|
3505
|
+
const agent = event.agent ?? "unknown";
|
|
3506
|
+
const dim = "\x1B[2m";
|
|
3507
|
+
const reset = "\x1B[0m";
|
|
3508
|
+
const cyan = "\x1B[36m";
|
|
3509
|
+
switch (event.type) {
|
|
3510
|
+
case "tool.before": {
|
|
3511
|
+
let icon;
|
|
3512
|
+
if (event.tool === "write" || event.tool === "edit")
|
|
3513
|
+
icon = "✏️ ";
|
|
3514
|
+
else if (event.tool === "read")
|
|
3515
|
+
icon = "\uD83D\uDD0D";
|
|
3516
|
+
else if (event.tool === "bash" || event.tool === "shell")
|
|
3517
|
+
icon = "\uD83C\uDFC3";
|
|
3518
|
+
else if (event.tool === "delegate")
|
|
3519
|
+
icon = "\uD83E\uDD16";
|
|
3520
|
+
else
|
|
3521
|
+
icon = "\uD83D\uDD27";
|
|
3522
|
+
const argStr = formatArgs(event.args);
|
|
3523
|
+
const thinking = event.thinking ? ` "${event.thinking}"` : "";
|
|
3524
|
+
return `${dim}[${time}]${reset} ${icon} ${cyan}${agent}${reset} → ${event.tool}(${argStr})${thinking}`;
|
|
3525
|
+
}
|
|
3526
|
+
case "tool.after": {
|
|
3527
|
+
let icon;
|
|
3528
|
+
let statusColor;
|
|
3529
|
+
if (event.status === "success") {
|
|
3530
|
+
icon = "✅";
|
|
3531
|
+
statusColor = "\x1B[32m";
|
|
3532
|
+
} else if (event.status === "error") {
|
|
3533
|
+
icon = "❌";
|
|
3534
|
+
statusColor = "\x1B[31m";
|
|
3535
|
+
} else if (event.status === "blocked") {
|
|
3536
|
+
icon = "⛔";
|
|
3537
|
+
statusColor = "\x1B[33m";
|
|
3538
|
+
} else {
|
|
3539
|
+
icon = "✅";
|
|
3540
|
+
statusColor = "\x1B[32m";
|
|
3541
|
+
}
|
|
3542
|
+
const argStr = formatArgs(event.args);
|
|
3543
|
+
const duration = event.duration_ms ? ` done in ${event.duration_ms}ms` : "";
|
|
3544
|
+
const error = event.error ? ` error: ${event.error}` : "";
|
|
3545
|
+
return `${dim}[${time}]${reset} ${icon} ${cyan}${agent}${reset} → ${event.tool}(${argStr})${statusColor}${duration}${error}${reset}`;
|
|
3546
|
+
}
|
|
3547
|
+
case "agent.delegated": {
|
|
3548
|
+
const thinking = event.thinking ? ` "${event.thinking}"` : "";
|
|
3549
|
+
return `${dim}[${time}]${reset} \uD83E\uDD16 ${cyan}${agent}${reset} → delegate(${thinking})`;
|
|
3550
|
+
}
|
|
3551
|
+
case "session.created":
|
|
3552
|
+
return `${dim}[${time}]${reset} \uD83D\uDCC2 session created${event.session_id ? ` (${event.session_id})` : ""}`;
|
|
3553
|
+
case "session.idle":
|
|
3554
|
+
return `${dim}[${time}]${reset} \uD83D\uDCA4 session idle${event.session_id ? ` (${event.session_id})` : ""}`;
|
|
3555
|
+
case "session.error":
|
|
3556
|
+
return `${dim}[${time}]${reset} ❌ session error${event.error ? `: ${event.error}` : ""}`;
|
|
3557
|
+
default:
|
|
3558
|
+
return `${dim}[${time}]${reset} ${event.type}`;
|
|
3559
|
+
}
|
|
3560
|
+
}
|
|
3561
|
+
function formatArgs(args) {
|
|
3562
|
+
if (!args)
|
|
3563
|
+
return "";
|
|
3564
|
+
const parts = [];
|
|
3565
|
+
for (const [key, value] of Object.entries(args)) {
|
|
3566
|
+
if (key === "filePath" || key === "path" || key === "file") {
|
|
3567
|
+
parts.push(String(value));
|
|
3568
|
+
} else if (key === "agent") {
|
|
3569
|
+
parts.push(`@${String(value)}`);
|
|
3570
|
+
}
|
|
3571
|
+
}
|
|
3572
|
+
return parts.join(", ");
|
|
3573
|
+
}
|
|
3574
|
+
|
|
3575
|
+
// src/hooks/event-log-hook.ts
|
|
3576
|
+
var toolStartTimes = new Map;
|
|
3577
|
+
var staleThresholdMs = 5 * 60 * 1000;
|
|
3578
|
+
var CLEANUP_INTERVAL = 50;
|
|
3579
|
+
var beforeHookCallCount = 0;
|
|
3580
|
+
function cleanupStaleToolStartTimes() {
|
|
3581
|
+
const now = Date.now();
|
|
3582
|
+
for (const [key, startTime] of toolStartTimes.entries()) {
|
|
3583
|
+
if (now - startTime > staleThresholdMs) {
|
|
3584
|
+
toolStartTimes.delete(key);
|
|
3585
|
+
}
|
|
3586
|
+
}
|
|
3587
|
+
}
|
|
3588
|
+
async function eventLogBeforeHook(ctx, toolInput, toolOutput) {
|
|
3589
|
+
const toolName = toolInput.tool ?? toolInput.name ?? "unknown";
|
|
3590
|
+
const sessionId = toolInput.sessionID ?? toolInput.sessionId ?? "unknown";
|
|
3591
|
+
const args = toolOutput?.args ?? toolInput?.args ?? {};
|
|
3592
|
+
const startKey = `${sessionId}:${toolName}`;
|
|
3593
|
+
beforeHookCallCount++;
|
|
3594
|
+
if (beforeHookCallCount >= CLEANUP_INTERVAL) {
|
|
3595
|
+
beforeHookCallCount = 0;
|
|
3596
|
+
cleanupStaleToolStartTimes();
|
|
3597
|
+
}
|
|
3598
|
+
toolStartTimes.set(startKey, Date.now());
|
|
3599
|
+
const event = {
|
|
3600
|
+
timestamp: new Date().toISOString(),
|
|
3601
|
+
type: "tool.before",
|
|
3602
|
+
agent: getCurrentAgent() ?? undefined,
|
|
3603
|
+
tool: toolName,
|
|
3604
|
+
args: sanitizeArgs(args),
|
|
3605
|
+
session_id: sessionId
|
|
3606
|
+
};
|
|
3607
|
+
logEvent(ctx.directory, event);
|
|
3608
|
+
}
|
|
3609
|
+
async function eventLogAfterHook(ctx, toolInput, toolOutput) {
|
|
3610
|
+
const toolName = toolInput.tool ?? toolInput.name ?? "unknown";
|
|
3611
|
+
const sessionId = toolInput.sessionID ?? toolInput.sessionId ?? "unknown";
|
|
3612
|
+
const args = toolOutput?.args ?? toolInput?.args ?? {};
|
|
3613
|
+
const startKey = `${sessionId}:${toolName}`;
|
|
3614
|
+
const startTime = toolStartTimes.get(startKey);
|
|
3615
|
+
const durationMs = startTime ? Date.now() - startTime : undefined;
|
|
3616
|
+
toolStartTimes.delete(startKey);
|
|
3617
|
+
let status = "success";
|
|
3618
|
+
let error;
|
|
3619
|
+
if (toolOutput?.error != null) {
|
|
3620
|
+
status = "error";
|
|
3621
|
+
error = typeof toolOutput.error === "string" ? toolOutput.error : String(toolOutput.error);
|
|
3622
|
+
} else if (toolOutput?.status === "error") {
|
|
3623
|
+
status = "error";
|
|
3624
|
+
error = typeof toolOutput.error === "string" ? toolOutput.error : "Unknown error";
|
|
3625
|
+
} else if (toolOutput?.status === "blocked") {
|
|
3626
|
+
status = "blocked";
|
|
3627
|
+
}
|
|
3628
|
+
const event = {
|
|
3629
|
+
timestamp: new Date().toISOString(),
|
|
3630
|
+
type: "tool.after",
|
|
3631
|
+
agent: getCurrentAgent() ?? undefined,
|
|
3632
|
+
tool: toolName,
|
|
3633
|
+
args: sanitizeArgs(args),
|
|
3634
|
+
duration_ms: durationMs,
|
|
3635
|
+
status,
|
|
3636
|
+
error,
|
|
3637
|
+
session_id: sessionId
|
|
3638
|
+
};
|
|
3639
|
+
logEvent(ctx.directory, event);
|
|
3640
|
+
}
|
|
3641
|
+
async function eventLogSessionHook(ctx, event) {
|
|
3642
|
+
const type = event?.type ?? "";
|
|
3643
|
+
const props = event?.properties ?? {};
|
|
3644
|
+
if (type === "session.created") {
|
|
3645
|
+
if (props.parentID) {
|
|
3646
|
+
const agentName = extractAgentFromEvent(props);
|
|
3647
|
+
setCurrentAgent(agentName);
|
|
3648
|
+
}
|
|
3649
|
+
const toolEvent = {
|
|
3650
|
+
timestamp: new Date().toISOString(),
|
|
3651
|
+
type: "session.created",
|
|
3652
|
+
session_id: props.id ?? props.sessionId ?? undefined
|
|
3653
|
+
};
|
|
3654
|
+
logEvent(ctx.directory, toolEvent);
|
|
3655
|
+
} else if (type === "session.idle") {
|
|
3656
|
+
if (props.parentID) {
|
|
3657
|
+
setCurrentAgent(null);
|
|
3658
|
+
}
|
|
3659
|
+
const toolEvent = {
|
|
3660
|
+
timestamp: new Date().toISOString(),
|
|
3661
|
+
type: "session.idle",
|
|
3662
|
+
session_id: props.id ?? props.sessionId ?? undefined
|
|
3663
|
+
};
|
|
3664
|
+
logEvent(ctx.directory, toolEvent);
|
|
3665
|
+
} else if (type === "session.error") {
|
|
3666
|
+
if (props.parentID) {
|
|
3667
|
+
setCurrentAgent(null);
|
|
3668
|
+
}
|
|
3669
|
+
const err = props.error;
|
|
3670
|
+
const errorMsg = (err && typeof err === "object" && "message" in err ? String(err.message) : undefined) ?? (typeof err === "string" ? err : undefined) ?? undefined;
|
|
3671
|
+
const toolEvent = {
|
|
3672
|
+
timestamp: new Date().toISOString(),
|
|
3673
|
+
type: "session.error",
|
|
3674
|
+
session_id: props.id ?? props.sessionId ?? undefined,
|
|
3675
|
+
error: errorMsg
|
|
3676
|
+
};
|
|
3677
|
+
logEvent(ctx.directory, toolEvent);
|
|
3678
|
+
}
|
|
3679
|
+
}
|
|
3680
|
+
function extractAgentFromEvent(props) {
|
|
3681
|
+
if (typeof props.agent === "string")
|
|
3682
|
+
return props.agent;
|
|
3683
|
+
if (typeof props.name === "string")
|
|
3684
|
+
return props.name;
|
|
3685
|
+
const title = typeof props.title === "string" ? props.title : "";
|
|
3686
|
+
const match = title.match(/^(.+)-delegate$/);
|
|
3687
|
+
if (match)
|
|
3688
|
+
return match[1];
|
|
3689
|
+
return "unknown";
|
|
3690
|
+
}
|
|
3691
|
+
|
|
4181
3692
|
// src/hooks/context-window-monitor.ts
|
|
4182
3693
|
var CONTEXT_WARNING_THRESHOLD = 0.7;
|
|
4183
3694
|
var DEFAULT_CONTEXT_LIMIT = Number(process.env.FLOWDECK_CONTEXT_LIMIT) || 200000;
|
|
@@ -4229,8 +3740,8 @@ function createContextWindowMonitorHook() {
|
|
|
4229
3740
|
}
|
|
4230
3741
|
|
|
4231
3742
|
// src/hooks/shell-env-hook.ts
|
|
4232
|
-
import { existsSync as
|
|
4233
|
-
import { join as
|
|
3743
|
+
import { existsSync as existsSync27, readFileSync as readFileSync26 } from "fs";
|
|
3744
|
+
import { join as join25 } from "path";
|
|
4234
3745
|
import { createRequire as createRequire2 } from "module";
|
|
4235
3746
|
var _version;
|
|
4236
3747
|
function getVersion() {
|
|
@@ -4266,7 +3777,7 @@ var MARKER_TO_LANG = {
|
|
|
4266
3777
|
};
|
|
4267
3778
|
function detectPackageManager(root) {
|
|
4268
3779
|
for (const [lockfile, pm] of Object.entries(LOCKFILE_TO_PM)) {
|
|
4269
|
-
if (
|
|
3780
|
+
if (existsSync27(join25(root, lockfile)))
|
|
4270
3781
|
return pm;
|
|
4271
3782
|
}
|
|
4272
3783
|
return;
|
|
@@ -4275,7 +3786,7 @@ function detectLanguages(root) {
|
|
|
4275
3786
|
const langs = [];
|
|
4276
3787
|
const seen = new Set;
|
|
4277
3788
|
for (const [marker, lang] of Object.entries(MARKER_TO_LANG)) {
|
|
4278
|
-
if (!seen.has(lang) &&
|
|
3789
|
+
if (!seen.has(lang) && existsSync27(join25(root, marker))) {
|
|
4279
3790
|
langs.push(lang);
|
|
4280
3791
|
seen.add(lang);
|
|
4281
3792
|
}
|
|
@@ -4283,11 +3794,11 @@ function detectLanguages(root) {
|
|
|
4283
3794
|
return langs;
|
|
4284
3795
|
}
|
|
4285
3796
|
function readCurrentPhase(root) {
|
|
4286
|
-
const statePath2 =
|
|
4287
|
-
if (!
|
|
3797
|
+
const statePath2 = join25(root, ".planning", "STATE.md");
|
|
3798
|
+
if (!existsSync27(statePath2))
|
|
4288
3799
|
return;
|
|
4289
3800
|
try {
|
|
4290
|
-
const content =
|
|
3801
|
+
const content = readFileSync26(statePath2, "utf-8");
|
|
4291
3802
|
const match = content.match(/phase:\s*(\S+)/i);
|
|
4292
3803
|
return match?.[1];
|
|
4293
3804
|
} catch {
|
|
@@ -4412,8 +3923,8 @@ function createSessionIdleHook(client, tracker) {
|
|
|
4412
3923
|
}
|
|
4413
3924
|
|
|
4414
3925
|
// src/hooks/compaction-hook.ts
|
|
4415
|
-
import { existsSync as
|
|
4416
|
-
import { join as
|
|
3926
|
+
import { existsSync as existsSync28, readFileSync as readFileSync27 } from "fs";
|
|
3927
|
+
import { join as join26 } from "path";
|
|
4417
3928
|
var STRUCTURED_SUMMARY_PROMPT = `
|
|
4418
3929
|
When summarizing this session, you MUST include the following sections:
|
|
4419
3930
|
|
|
@@ -4454,10 +3965,10 @@ For each: agent name, status, description, session_id.
|
|
|
4454
3965
|
var _lastInjected = new Map;
|
|
4455
3966
|
function readPlanningState2(directory) {
|
|
4456
3967
|
const sp = statePath(directory);
|
|
4457
|
-
if (!
|
|
3968
|
+
if (!existsSync28(sp))
|
|
4458
3969
|
return null;
|
|
4459
3970
|
try {
|
|
4460
|
-
const content =
|
|
3971
|
+
const content = readFileSync27(sp, "utf-8");
|
|
4461
3972
|
const parsed = parseState(content);
|
|
4462
3973
|
const version = typeof parsed.summaryVersion === "number" ? parsed.summaryVersion : 0;
|
|
4463
3974
|
return { content: content.slice(0, 1500), version };
|
|
@@ -4486,15 +3997,15 @@ function createCompactionHook(ctx, tracker) {
|
|
|
4486
3997
|
sections.push(`_State unchanged since last compaction. summaryVersion=${currentStateVersion}_`);
|
|
4487
3998
|
sections.push("");
|
|
4488
3999
|
}
|
|
4489
|
-
const indexPath2 =
|
|
4490
|
-
if (indexChanged &&
|
|
4000
|
+
const indexPath2 = join26(ctx.directory, ".planning", "CODEBASE_INDEX.md");
|
|
4001
|
+
if (indexChanged && existsSync28(indexPath2)) {
|
|
4491
4002
|
try {
|
|
4492
|
-
const indexContent =
|
|
4003
|
+
const indexContent = readFileSync27(indexPath2, "utf-8");
|
|
4493
4004
|
const indexSummary = "\n## Codebase Index\n```\n" + indexContent.slice(0, 800) + "\n```\n";
|
|
4494
4005
|
sections.push(indexSummary);
|
|
4495
4006
|
sections.push("");
|
|
4496
4007
|
} catch {}
|
|
4497
|
-
} else if (
|
|
4008
|
+
} else if (existsSync28(indexPath2)) {
|
|
4498
4009
|
sections.push(`## Codebase Index (unchanged, v${currentIndexVersion})`);
|
|
4499
4010
|
sections.push(`_Index unchanged since last compaction. summaryVersion=${currentIndexVersion}_`);
|
|
4500
4011
|
sections.push("");
|
|
@@ -4545,12 +4056,9 @@ var ALWAYS_ALLOWED = new Set([
|
|
|
4545
4056
|
"council",
|
|
4546
4057
|
"planning-state",
|
|
4547
4058
|
"codebase-state",
|
|
4548
|
-
"workspace-state",
|
|
4549
4059
|
"repo-memory",
|
|
4550
4060
|
"decision-trace",
|
|
4551
4061
|
"policy-engine",
|
|
4552
|
-
"context-generator",
|
|
4553
|
-
"create-skill",
|
|
4554
4062
|
"reflect"
|
|
4555
4063
|
]);
|
|
4556
4064
|
function isDelegationTool(name) {
|
|
@@ -4661,7 +4169,7 @@ async function runAutoLearner(client, directory, appLog) {
|
|
|
4661
4169
|
parts: [
|
|
4662
4170
|
{
|
|
4663
4171
|
type: "text",
|
|
4664
|
-
text: "Run your automated self-improvement routine: call `reflect`, " + "identify patterns, and
|
|
4172
|
+
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
4173
|
}
|
|
4666
4174
|
],
|
|
4667
4175
|
tools: { question: false }
|
|
@@ -4895,8 +4403,8 @@ Please advise.
|
|
|
4895
4403
|
## Self-Learning
|
|
4896
4404
|
|
|
4897
4405
|
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
|
|
4406
|
+
1. After the task completes successfully, write a new skill markdown file under \`src/skills/<name>/SKILL.md\` to capture the pattern
|
|
4407
|
+
2. Use a descriptive kebab-case name for the directory, a one-sentence description in the frontmatter, and structured Markdown content
|
|
4900
4408
|
3. Include: When to Activate, Steps, Examples, and Pitfalls sections
|
|
4901
4409
|
|
|
4902
4410
|
Do NOT create a skill for routine tasks. Only capture genuinely novel or reusable patterns.`;
|
|
@@ -7042,7 +6550,6 @@ You receive a structured context with:
|
|
|
7042
6550
|
- \`file_path\`: optional specific file being changed
|
|
7043
6551
|
- \`trust_score\`: patch trust score (0–100; 80+ = safe, 40–79 = review-required, <40 = high-risk)
|
|
7044
6552
|
- \`trust_signals\`: list of risk signals from the patch trust scorer
|
|
7045
|
-
- \`volatile_zones\`: paths marked as volatile or critical in VOLATILITY.json
|
|
7046
6553
|
- \`prior_failures\`: failure entries from FAILURES.json that match this change
|
|
7047
6554
|
- \`regression_categories\`: predicted regression categories for this change
|
|
7048
6555
|
- \`confidence\`: system confidence score (0–100; based on how much codebase context data exists)
|
|
@@ -7501,7 +7008,7 @@ var AUTO_LEARNER_PROMPT = `You run automatically after a coding session to captu
|
|
|
7501
7008
|
- Novel solutions that took non-obvious reasoning
|
|
7502
7009
|
- Recurring tool sequences that indicate a reusable workflow
|
|
7503
7010
|
- Knowledge gaps that had to be worked out from scratch
|
|
7504
|
-
3. For each valuable pattern,
|
|
7011
|
+
3. For each valuable pattern, write a skill markdown file under \`src/skills/<name>/SKILL.md\` immediately.
|
|
7505
7012
|
4. If nothing is worth capturing, output exactly: "No new skills identified."
|
|
7506
7013
|
5. End with a one-line summary: "Auto-learn complete: N skill(s) created."
|
|
7507
7014
|
|
|
@@ -7866,12 +7373,9 @@ var CONTRACTS = [
|
|
|
7866
7373
|
"council",
|
|
7867
7374
|
"planning-state",
|
|
7868
7375
|
"codebase-state",
|
|
7869
|
-
"workspace-state",
|
|
7870
7376
|
"repo-memory",
|
|
7871
7377
|
"decision-trace",
|
|
7872
7378
|
"policy-engine",
|
|
7873
|
-
"context-generator",
|
|
7874
|
-
"create-skill",
|
|
7875
7379
|
"reflect"
|
|
7876
7380
|
],
|
|
7877
7381
|
forbiddenActions: [
|
|
@@ -7906,7 +7410,7 @@ var CONTRACTS = [
|
|
|
7906
7410
|
allowedTaskTypes: ["planning", "task-breakdown", "step-decomposition"],
|
|
7907
7411
|
requiredInputs: ["task description or STATE.md"],
|
|
7908
7412
|
expectedOutputFields: ["steps", "phase"],
|
|
7909
|
-
allowedTools: ["read", "glob", "grep", "planning-state"
|
|
7413
|
+
allowedTools: ["read", "glob", "grep", "planning-state"],
|
|
7910
7414
|
forbiddenActions: [
|
|
7911
7415
|
"write source files",
|
|
7912
7416
|
"run bash commands",
|
|
@@ -8422,7 +7926,6 @@ function runSupervisorReview(directory, targetName, ctx = {}, clarificationQuest
|
|
|
8422
7926
|
reviewPhase,
|
|
8423
7927
|
timestamp: timestamp2
|
|
8424
7928
|
};
|
|
8425
|
-
_emitTelemetry(directory, decision2, ctx);
|
|
8426
7929
|
return decision2;
|
|
8427
7930
|
}
|
|
8428
7931
|
const policyResult = targetType === "command" ? checkCommandPolicy(targetName, ctx) : checkAgentPolicy(targetName, ctx);
|
|
@@ -8444,7 +7947,6 @@ function runSupervisorReview(directory, targetName, ctx = {}, clarificationQuest
|
|
|
8444
7947
|
timestamp: timestamp2,
|
|
8445
7948
|
...escalationQuestion ? { clarificationQuestion: escalationQuestion } : {}
|
|
8446
7949
|
};
|
|
8447
|
-
_emitTelemetry(directory, supervisorDecision, ctx);
|
|
8448
7950
|
return supervisorDecision;
|
|
8449
7951
|
}
|
|
8450
7952
|
function shouldProceed(decision, mode, canBlock) {
|
|
@@ -8457,33 +7959,12 @@ function shouldProceed(decision, mode, canBlock) {
|
|
|
8457
7959
|
}
|
|
8458
7960
|
return decision.decision !== "block" || decision.confidenceScore > 0.3;
|
|
8459
7961
|
}
|
|
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
7962
|
|
|
8482
7963
|
// src/index.ts
|
|
8483
7964
|
function lazyLoadRulePaths(projectRoot) {
|
|
8484
|
-
const __dir =
|
|
8485
|
-
const rulesDir =
|
|
8486
|
-
if (!
|
|
7965
|
+
const __dir = dirname3(fileURLToPath2(import.meta.url));
|
|
7966
|
+
const rulesDir = join27(__dir, "..", "src", "rules");
|
|
7967
|
+
if (!existsSync29(rulesDir))
|
|
8487
7968
|
return { paths: [], diagnostics: "[LazyRuleLoader] rules directory not found" };
|
|
8488
7969
|
const detectedLanguages = detectProjectLanguages(projectRoot);
|
|
8489
7970
|
const paths = getStartupRulePaths(rulesDir, detectedLanguages);
|
|
@@ -8492,17 +7973,17 @@ function lazyLoadRulePaths(projectRoot) {
|
|
|
8492
7973
|
return { paths, diagnostics };
|
|
8493
7974
|
}
|
|
8494
7975
|
function loadCommands() {
|
|
8495
|
-
const __dir =
|
|
8496
|
-
const commandsDir =
|
|
8497
|
-
if (!
|
|
7976
|
+
const __dir = dirname3(fileURLToPath2(import.meta.url));
|
|
7977
|
+
const commandsDir = join27(__dir, "..", "src", "commands");
|
|
7978
|
+
if (!existsSync29(commandsDir))
|
|
8498
7979
|
return {};
|
|
8499
7980
|
const commands = {};
|
|
8500
7981
|
try {
|
|
8501
|
-
for (const file of
|
|
7982
|
+
for (const file of readdirSync4(commandsDir)) {
|
|
8502
7983
|
if (!file.endsWith(".md"))
|
|
8503
7984
|
continue;
|
|
8504
7985
|
const name = basename2(file, ".md");
|
|
8505
|
-
const raw =
|
|
7986
|
+
const raw = readFileSync28(join27(commandsDir, file), "utf-8");
|
|
8506
7987
|
let description;
|
|
8507
7988
|
let template = raw;
|
|
8508
7989
|
const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
|
|
@@ -8520,10 +8001,8 @@ function loadCommands() {
|
|
|
8520
8001
|
var plugin = async (input, _options) => {
|
|
8521
8002
|
const { directory, client, worktree } = input;
|
|
8522
8003
|
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);
|
|
8004
|
+
const runPipelineTool = createRunPipelineTool(client);
|
|
8005
|
+
const delegateTool = createDelegateTool(client);
|
|
8527
8006
|
const councilTool = createCouncilTool(client);
|
|
8528
8007
|
const fileTracker = new SessionFileTracker;
|
|
8529
8008
|
const { fileEdited, fileWatcherUpdated } = createFileTrackerHooks(fileTracker);
|
|
@@ -8587,8 +8066,8 @@ var plugin = async (input, _options) => {
|
|
|
8587
8066
|
}
|
|
8588
8067
|
}
|
|
8589
8068
|
}
|
|
8590
|
-
const skillsDir =
|
|
8591
|
-
if (
|
|
8069
|
+
const skillsDir = join27(dirname3(fileURLToPath2(import.meta.url)), "..", "src", "skills");
|
|
8070
|
+
if (existsSync29(skillsDir)) {
|
|
8592
8071
|
const cfgAny = cfg;
|
|
8593
8072
|
if (!cfgAny.skills || typeof cfgAny.skills !== "object") {
|
|
8594
8073
|
cfgAny.skills = { paths: [] };
|
|
@@ -8617,18 +8096,14 @@ var plugin = async (input, _options) => {
|
|
|
8617
8096
|
tool: {
|
|
8618
8097
|
"planning-state": planningStateTool,
|
|
8619
8098
|
"codebase-state": codebaseStateTool,
|
|
8620
|
-
"workspace-state": workspaceStateTool,
|
|
8621
8099
|
"run-pipeline": runPipelineTool,
|
|
8622
8100
|
delegate: delegateTool,
|
|
8623
8101
|
"repo-memory": repoMemoryTool,
|
|
8624
8102
|
"failure-replay": failureReplayTool,
|
|
8625
8103
|
"decision-trace": decisionTraceTool,
|
|
8626
|
-
"volatility-map": volatilityMapTool,
|
|
8627
8104
|
"policy-engine": policyEngineTool,
|
|
8628
8105
|
"hash-edit": hashEditTool,
|
|
8629
8106
|
council: councilTool,
|
|
8630
|
-
"context-generator": contextGeneratorTool,
|
|
8631
|
-
"create-skill": createSkillTool,
|
|
8632
8107
|
reflect: reflectTool,
|
|
8633
8108
|
codegraph: codegraphTool,
|
|
8634
8109
|
"load-rules": loadRulesTool,
|
|
@@ -8641,17 +8116,18 @@ var plugin = async (input, _options) => {
|
|
|
8641
8116
|
"file.watcher.updated": fileWatcherUpdated,
|
|
8642
8117
|
"experimental.session.compacting": compactionHook,
|
|
8643
8118
|
"command.execute.before": async (input2, _output) => {
|
|
8644
|
-
activityReporter.reportCommandStarted(input2.command);
|
|
8645
8119
|
lastExecutedCommand = input2.command;
|
|
8646
8120
|
},
|
|
8647
8121
|
"permission.ask": async (input2, _output) => {
|
|
8648
8122
|
notifyPermissionNeeded(input2.title);
|
|
8649
|
-
activityReporter.reportWaitingForApproval(input2.title);
|
|
8650
8123
|
},
|
|
8651
8124
|
event: async ({ event }) => {
|
|
8652
8125
|
const type = event?.type ?? "";
|
|
8653
8126
|
if (type === "session.created" || type === "session.started") {
|
|
8654
8127
|
await sessionStartHook({ directory });
|
|
8128
|
+
if (type === "session.created") {
|
|
8129
|
+
await eventLogSessionHook({ directory }, event);
|
|
8130
|
+
}
|
|
8655
8131
|
}
|
|
8656
8132
|
if (type === "command.executed") {
|
|
8657
8133
|
const commandName = event?.properties?.name ?? "";
|
|
@@ -8662,9 +8138,9 @@ var plugin = async (input, _options) => {
|
|
|
8662
8138
|
await contextMonitor.event({ event });
|
|
8663
8139
|
orchestratorGuard.onEvent(event);
|
|
8664
8140
|
if (type === "session.idle") {
|
|
8141
|
+
await eventLogSessionHook({ directory }, event);
|
|
8665
8142
|
const hasEdits = fileTracker.getEditedPaths().length > 0;
|
|
8666
8143
|
if (lastExecutedCommand) {
|
|
8667
|
-
activityReporter.reportCommandCompleted(lastExecutedCommand, hasEdits);
|
|
8668
8144
|
lastExecutedCommand = null;
|
|
8669
8145
|
}
|
|
8670
8146
|
notifCtrl.onSessionIdle(hasEdits);
|
|
@@ -8676,6 +8152,7 @@ var plugin = async (input, _options) => {
|
|
|
8676
8152
|
}
|
|
8677
8153
|
}
|
|
8678
8154
|
if (type === "session.error") {
|
|
8155
|
+
await eventLogSessionHook({ directory }, event);
|
|
8679
8156
|
lastExecutedCommand = null;
|
|
8680
8157
|
const err = event?.properties?.error;
|
|
8681
8158
|
const errorMsg = (err && typeof err === "object" && "message" in err ? String(err.message) : undefined) ?? (typeof err === "string" ? err : undefined) ?? "An unexpected error occurred";
|
|
@@ -8721,15 +8198,15 @@ var plugin = async (input, _options) => {
|
|
|
8721
8198
|
}
|
|
8722
8199
|
}
|
|
8723
8200
|
}
|
|
8724
|
-
await telemetryHook({ directory }, toolInput, toolOutput, activityReporter);
|
|
8725
8201
|
await approvalHook({ directory }, toolInput, toolOutput);
|
|
8726
8202
|
await guardRailsHook({ directory }, toolInput, toolOutput);
|
|
8727
8203
|
await toolGuardHook({ directory }, toolInput, toolOutput);
|
|
8728
8204
|
await patchTrustHook({ directory }, toolInput, toolOutput);
|
|
8729
8205
|
await decisionTraceHook({ directory }, toolInput, toolOutput);
|
|
8206
|
+
await eventLogBeforeHook({ directory }, toolInput, toolOutput);
|
|
8730
8207
|
},
|
|
8731
8208
|
"tool.execute.after": async (toolInput, toolOutput) => {
|
|
8732
|
-
await
|
|
8209
|
+
await eventLogAfterHook({ directory }, toolInput, toolOutput);
|
|
8733
8210
|
const afterToolName = toolInput.tool ?? toolInput.name ?? "";
|
|
8734
8211
|
if (afterToolName === "delegate" || afterToolName === "run-pipeline") {
|
|
8735
8212
|
try {
|