@dv.nghiem/flowdeck 0.4.3 → 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 +634 -823
- 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.map +1 -1
- 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 -32
- 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/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 -40
- 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}`;
|
|
@@ -873,18 +722,18 @@ function extractText(parts) {
|
|
|
873
722
|
`);
|
|
874
723
|
}
|
|
875
724
|
function createRunPipelineTool(client) {
|
|
876
|
-
return
|
|
725
|
+
return tool3({
|
|
877
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.",
|
|
878
727
|
args: {
|
|
879
|
-
steps:
|
|
880
|
-
agent:
|
|
881
|
-
prompt:
|
|
882
|
-
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()
|
|
883
732
|
})),
|
|
884
|
-
initial_context:
|
|
885
|
-
abort_on_failure:
|
|
886
|
-
retry_attempts:
|
|
887
|
-
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()
|
|
888
737
|
},
|
|
889
738
|
async execute(args, context) {
|
|
890
739
|
const startTime = Date.now();
|
|
@@ -893,6 +742,7 @@ function createRunPipelineTool(client) {
|
|
|
893
742
|
let aborted = false;
|
|
894
743
|
const retryAttempts = typeof args.retry_attempts === "number" ? args.retry_attempts : 1;
|
|
895
744
|
const maxRetries = Math.max(0, Math.floor(retryAttempts));
|
|
745
|
+
const totalSteps = args.steps.length;
|
|
896
746
|
let inflightChildId = null;
|
|
897
747
|
const abortHandler = () => {
|
|
898
748
|
if (inflightChildId) {
|
|
@@ -904,7 +754,8 @@ function createRunPipelineTool(client) {
|
|
|
904
754
|
};
|
|
905
755
|
context.abort.addEventListener("abort", abortHandler);
|
|
906
756
|
try {
|
|
907
|
-
for (
|
|
757
|
+
for (let stepIdx = 0;stepIdx < args.steps.length; stepIdx++) {
|
|
758
|
+
const step = args.steps[stepIdx];
|
|
908
759
|
if (context.abort.aborted) {
|
|
909
760
|
aborted = true;
|
|
910
761
|
break;
|
|
@@ -978,9 +829,10 @@ ${step.prompt}` : step.prompt;
|
|
|
978
829
|
} finally {
|
|
979
830
|
context.abort.removeEventListener("abort", abortHandler);
|
|
980
831
|
}
|
|
832
|
+
const totalDuration = Date.now() - startTime;
|
|
981
833
|
return JSON.stringify({
|
|
982
834
|
steps: trace,
|
|
983
|
-
total_duration_ms:
|
|
835
|
+
total_duration_ms: totalDuration,
|
|
984
836
|
aborted
|
|
985
837
|
});
|
|
986
838
|
}
|
|
@@ -988,13 +840,13 @@ ${step.prompt}` : step.prompt;
|
|
|
988
840
|
}
|
|
989
841
|
|
|
990
842
|
// src/tools/delegate.ts
|
|
991
|
-
import { tool as
|
|
992
|
-
import { existsSync as
|
|
843
|
+
import { tool as tool4 } from "@opencode-ai/plugin";
|
|
844
|
+
import { existsSync as existsSync10, readFileSync as readFileSync10 } from "fs";
|
|
993
845
|
|
|
994
846
|
// src/services/prompt-cache.ts
|
|
995
847
|
import { createHash } from "crypto";
|
|
996
|
-
import { existsSync as
|
|
997
|
-
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";
|
|
998
850
|
var CACHEABLE_AGENTS = new Set([
|
|
999
851
|
"researcher",
|
|
1000
852
|
"code-explorer",
|
|
@@ -1008,17 +860,17 @@ var CACHE_DIR_NAME = "prompt-cache";
|
|
|
1008
860
|
var MAX_CACHE_ENTRIES = 200;
|
|
1009
861
|
var DEFAULT_TTL_MS = 30 * 60 * 1000;
|
|
1010
862
|
function cacheDir(dir) {
|
|
1011
|
-
return
|
|
863
|
+
return join6(codebaseDir(dir), CACHE_DIR_NAME);
|
|
1012
864
|
}
|
|
1013
865
|
function entryPath(dir, key) {
|
|
1014
|
-
return
|
|
866
|
+
return join6(cacheDir(dir), `${key}.json`);
|
|
1015
867
|
}
|
|
1016
868
|
function readEntry(dir, key, stateVersion, indexVersion) {
|
|
1017
869
|
const path = entryPath(dir, key);
|
|
1018
|
-
if (!
|
|
870
|
+
if (!existsSync6(path))
|
|
1019
871
|
return null;
|
|
1020
872
|
try {
|
|
1021
|
-
const entry = JSON.parse(
|
|
873
|
+
const entry = JSON.parse(readFileSync6(path, "utf-8"));
|
|
1022
874
|
const age = Date.now() - new Date(entry.created_at).getTime();
|
|
1023
875
|
if (age > entry.ttl_ms)
|
|
1024
876
|
return null;
|
|
@@ -1066,7 +918,7 @@ function setCached(dir, agent, prompt, context, stateVersion, indexVersion, resp
|
|
|
1066
918
|
if (!CACHEABLE_AGENTS.has(agent))
|
|
1067
919
|
return;
|
|
1068
920
|
const cd = cacheDir(dir);
|
|
1069
|
-
if (!
|
|
921
|
+
if (!existsSync6(cd))
|
|
1070
922
|
mkdirSync3(cd, { recursive: true });
|
|
1071
923
|
const key = hashKey(agent, prompt, context, stateVersion, indexVersion);
|
|
1072
924
|
const entry = {
|
|
@@ -1078,21 +930,21 @@ function setCached(dir, agent, prompt, context, stateVersion, indexVersion, resp
|
|
|
1078
930
|
ttl_ms,
|
|
1079
931
|
response
|
|
1080
932
|
};
|
|
1081
|
-
|
|
933
|
+
writeFileSync5(entryPath(dir, key), JSON.stringify(entry, null, 2), "utf-8");
|
|
1082
934
|
pruneExpired(dir);
|
|
1083
935
|
}
|
|
1084
936
|
function pruneExpired(dir) {
|
|
1085
937
|
const cd = cacheDir(dir);
|
|
1086
|
-
if (!
|
|
938
|
+
if (!existsSync6(cd))
|
|
1087
939
|
return;
|
|
1088
940
|
try {
|
|
1089
941
|
const files = readdirSync3(cd).filter((f) => f.endsWith(".json"));
|
|
1090
942
|
const now = Date.now();
|
|
1091
943
|
const entries = [];
|
|
1092
944
|
for (const f of files) {
|
|
1093
|
-
const p =
|
|
945
|
+
const p = join6(cd, f);
|
|
1094
946
|
try {
|
|
1095
|
-
const entry = JSON.parse(
|
|
947
|
+
const entry = JSON.parse(readFileSync6(p, "utf-8"));
|
|
1096
948
|
const age = now - new Date(entry.created_at).getTime();
|
|
1097
949
|
entries.push({ path: p, created_at: new Date(entry.created_at).getTime(), expired: age > entry.ttl_ms });
|
|
1098
950
|
} catch {
|
|
@@ -1119,15 +971,15 @@ function pruneExpired(dir) {
|
|
|
1119
971
|
}
|
|
1120
972
|
|
|
1121
973
|
// src/tools/codebase-index.ts
|
|
1122
|
-
import { readFileSync as
|
|
1123
|
-
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";
|
|
1124
976
|
var CODEBASE_INDEX_FILE = "CODEBASE_INDEX.md";
|
|
1125
977
|
function indexPath(dir) {
|
|
1126
|
-
return
|
|
978
|
+
return join7(planningDir(dir), CODEBASE_INDEX_FILE);
|
|
1127
979
|
}
|
|
1128
980
|
function readCodebaseIndex(dir) {
|
|
1129
981
|
const path = indexPath(dir);
|
|
1130
|
-
if (!
|
|
982
|
+
if (!existsSync7(path)) {
|
|
1131
983
|
return {
|
|
1132
984
|
exists: false,
|
|
1133
985
|
lastUpdatedAt: "",
|
|
@@ -1141,7 +993,7 @@ function readCodebaseIndex(dir) {
|
|
|
1141
993
|
};
|
|
1142
994
|
}
|
|
1143
995
|
try {
|
|
1144
|
-
const content =
|
|
996
|
+
const content = readFileSync7(path, "utf-8");
|
|
1145
997
|
return parseCodebaseIndexContent(content);
|
|
1146
998
|
} catch {
|
|
1147
999
|
return {
|
|
@@ -1217,17 +1069,17 @@ function parseCodebaseIndexContent(content) {
|
|
|
1217
1069
|
}
|
|
1218
1070
|
|
|
1219
1071
|
// src/services/token-metrics.ts
|
|
1220
|
-
import { existsSync as
|
|
1221
|
-
import { join as
|
|
1072
|
+
import { existsSync as existsSync8, readFileSync as readFileSync8, appendFileSync, mkdirSync as mkdirSync5 } from "fs";
|
|
1073
|
+
import { join as join8 } from "path";
|
|
1222
1074
|
function estimateTokens(text) {
|
|
1223
1075
|
return Math.ceil(text.length / 4);
|
|
1224
1076
|
}
|
|
1225
1077
|
function metricsPath(dir) {
|
|
1226
|
-
return
|
|
1078
|
+
return join8(codebaseDir(dir), "TOKEN_METRICS.jsonl");
|
|
1227
1079
|
}
|
|
1228
1080
|
function appendEvent(dir, event) {
|
|
1229
1081
|
const cd = codebaseDir(dir);
|
|
1230
|
-
if (!
|
|
1082
|
+
if (!existsSync8(cd))
|
|
1231
1083
|
mkdirSync5(cd, { recursive: true });
|
|
1232
1084
|
appendFileSync(metricsPath(dir), JSON.stringify(event) + `
|
|
1233
1085
|
`, "utf-8");
|
|
@@ -1285,23 +1137,23 @@ function recordRetryCall(dir, workflow_id, stage, inputText, outputText, agent,
|
|
|
1285
1137
|
var _workflowTimers = new Map;
|
|
1286
1138
|
|
|
1287
1139
|
// src/config/loader.ts
|
|
1288
|
-
import { existsSync as
|
|
1289
|
-
import { join as
|
|
1140
|
+
import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
|
|
1141
|
+
import { join as join9 } from "path";
|
|
1290
1142
|
import { homedir } from "os";
|
|
1291
1143
|
var CONFIG_FILENAME = "flowdeck.json";
|
|
1292
1144
|
function getGlobalConfigDir() {
|
|
1293
|
-
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"));
|
|
1294
1146
|
}
|
|
1295
1147
|
function loadFlowDeckConfig(directory) {
|
|
1296
1148
|
const candidates = [];
|
|
1297
1149
|
if (directory) {
|
|
1298
|
-
candidates.push(
|
|
1150
|
+
candidates.push(join9(directory, ".opencode", CONFIG_FILENAME));
|
|
1299
1151
|
}
|
|
1300
|
-
candidates.push(
|
|
1152
|
+
candidates.push(join9(getGlobalConfigDir(), CONFIG_FILENAME));
|
|
1301
1153
|
for (const configPath of candidates) {
|
|
1302
|
-
if (
|
|
1154
|
+
if (existsSync9(configPath)) {
|
|
1303
1155
|
try {
|
|
1304
|
-
const content =
|
|
1156
|
+
const content = readFileSync9(configPath, "utf-8");
|
|
1305
1157
|
return JSON.parse(content);
|
|
1306
1158
|
} catch {
|
|
1307
1159
|
console.warn(`[flowdeck] Failed to load config from ${configPath}`);
|
|
@@ -1387,18 +1239,18 @@ function extractText2(parts) {
|
|
|
1387
1239
|
`);
|
|
1388
1240
|
}
|
|
1389
1241
|
function createDelegateTool(client) {
|
|
1390
|
-
return
|
|
1242
|
+
return tool4({
|
|
1391
1243
|
description: "Delegate a task to a single agent via a child session. Returns the agent's output.",
|
|
1392
1244
|
args: {
|
|
1393
|
-
agent:
|
|
1394
|
-
prompt:
|
|
1395
|
-
context:
|
|
1396
|
-
task_type:
|
|
1397
|
-
retry_attempts:
|
|
1398
|
-
safe_to_cache:
|
|
1399
|
-
cache_ttl_ms:
|
|
1400
|
-
workflow_id:
|
|
1401
|
-
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()
|
|
1402
1254
|
},
|
|
1403
1255
|
async execute(args, context) {
|
|
1404
1256
|
const startTime = Date.now();
|
|
@@ -1423,7 +1275,7 @@ ${args.prompt}` : args.prompt;
|
|
|
1423
1275
|
if (safe_to_cache) {
|
|
1424
1276
|
const index = readCodebaseIndex(context.directory);
|
|
1425
1277
|
const sp = statePath(context.directory);
|
|
1426
|
-
const rawState =
|
|
1278
|
+
const rawState = existsSync10(sp) ? readFileSync10(sp, "utf-8") : "";
|
|
1427
1279
|
const state = rawState ? parseState(rawState) : {};
|
|
1428
1280
|
stateVersion = typeof state.summaryVersion === "number" ? state.summaryVersion : 0;
|
|
1429
1281
|
indexVersion = typeof index.summaryVersion === "number" ? index.summaryVersion : 0;
|
|
@@ -1491,12 +1343,13 @@ ${args.prompt}` : args.prompt;
|
|
|
1491
1343
|
retriesUsed++;
|
|
1492
1344
|
}
|
|
1493
1345
|
if (!promptRes || promptRes.error) {
|
|
1346
|
+
const errMsg = `Prompt failed: ${promptRes?.error?.detail ?? "unknown"}`;
|
|
1494
1347
|
recordRun(context.directory, args.agent, "", taskType, false, Date.now() - startTime);
|
|
1495
1348
|
return JSON.stringify({
|
|
1496
1349
|
agent: args.agent,
|
|
1497
1350
|
session_id: childId,
|
|
1498
1351
|
success: false,
|
|
1499
|
-
error:
|
|
1352
|
+
error: errMsg,
|
|
1500
1353
|
task_type: taskType,
|
|
1501
1354
|
model: "",
|
|
1502
1355
|
retries_used: retriesUsed,
|
|
@@ -1505,12 +1358,13 @@ ${args.prompt}` : args.prompt;
|
|
|
1505
1358
|
}
|
|
1506
1359
|
const info = promptRes.data?.info;
|
|
1507
1360
|
if (info?.error) {
|
|
1361
|
+
const errMsg = `Agent error: ${JSON.stringify(info.error)}`;
|
|
1508
1362
|
recordRun(context.directory, args.agent, "", taskType, false, Date.now() - startTime);
|
|
1509
1363
|
return JSON.stringify({
|
|
1510
1364
|
agent: args.agent,
|
|
1511
1365
|
session_id: childId,
|
|
1512
1366
|
success: false,
|
|
1513
|
-
error:
|
|
1367
|
+
error: errMsg,
|
|
1514
1368
|
task_type: taskType,
|
|
1515
1369
|
model: "",
|
|
1516
1370
|
retries_used: retriesUsed,
|
|
@@ -1543,53 +1397,53 @@ ${args.prompt}` : args.prompt;
|
|
|
1543
1397
|
}
|
|
1544
1398
|
|
|
1545
1399
|
// src/tools/repo-memory.ts
|
|
1546
|
-
import { tool as
|
|
1547
|
-
import { readFileSync as
|
|
1548
|
-
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";
|
|
1549
1403
|
var MEMORY_FILE = "MEMORY.json";
|
|
1550
1404
|
function memoryPath(directory) {
|
|
1551
|
-
return
|
|
1405
|
+
return join10(codebaseDir(directory), MEMORY_FILE);
|
|
1552
1406
|
}
|
|
1553
1407
|
function emptyMemory() {
|
|
1554
1408
|
return { version: "1.0", last_updated: new Date().toISOString(), nodes: {} };
|
|
1555
1409
|
}
|
|
1556
1410
|
function readMemory(directory) {
|
|
1557
1411
|
const p = memoryPath(directory);
|
|
1558
|
-
if (!
|
|
1412
|
+
if (!existsSync11(p))
|
|
1559
1413
|
return emptyMemory();
|
|
1560
1414
|
try {
|
|
1561
|
-
return JSON.parse(
|
|
1415
|
+
return JSON.parse(readFileSync11(p, "utf-8"));
|
|
1562
1416
|
} catch {
|
|
1563
1417
|
return emptyMemory();
|
|
1564
1418
|
}
|
|
1565
1419
|
}
|
|
1566
1420
|
function writeMemory(directory, memory) {
|
|
1567
1421
|
const base = codebaseDir(directory);
|
|
1568
|
-
if (!
|
|
1422
|
+
if (!existsSync11(base))
|
|
1569
1423
|
mkdirSync6(base, { recursive: true });
|
|
1570
1424
|
memory.last_updated = new Date().toISOString();
|
|
1571
|
-
|
|
1425
|
+
writeFileSync7(memoryPath(directory), JSON.stringify(memory, null, 2), "utf-8");
|
|
1572
1426
|
}
|
|
1573
|
-
var repoMemoryTool =
|
|
1427
|
+
var repoMemoryTool = tool5({
|
|
1574
1428
|
description: "Repo Memory Graph: read/write/query persistent architecture graph in .codebase/MEMORY.json (modules, dependencies, ownership, bug history, conventions)",
|
|
1575
1429
|
args: {
|
|
1576
|
-
action:
|
|
1577
|
-
node_id:
|
|
1578
|
-
node:
|
|
1579
|
-
type:
|
|
1580
|
-
path:
|
|
1581
|
-
owner:
|
|
1582
|
-
tags:
|
|
1583
|
-
dependencies:
|
|
1584
|
-
dependents:
|
|
1585
|
-
bug_history:
|
|
1586
|
-
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())
|
|
1587
1441
|
}).optional(),
|
|
1588
|
-
query:
|
|
1589
|
-
type:
|
|
1590
|
-
owner:
|
|
1591
|
-
tag:
|
|
1592
|
-
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()
|
|
1593
1447
|
}).optional()
|
|
1594
1448
|
},
|
|
1595
1449
|
async execute(args, context) {
|
|
@@ -1644,50 +1498,50 @@ var repoMemoryTool = tool6({
|
|
|
1644
1498
|
});
|
|
1645
1499
|
|
|
1646
1500
|
// src/tools/failure-replay.ts
|
|
1647
|
-
import { tool as
|
|
1648
|
-
import { readFileSync as
|
|
1649
|
-
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";
|
|
1650
1504
|
var FAILURES_FILE = "FAILURES.json";
|
|
1651
1505
|
function failuresPath(directory) {
|
|
1652
|
-
return
|
|
1506
|
+
return join11(codebaseDir(directory), FAILURES_FILE);
|
|
1653
1507
|
}
|
|
1654
1508
|
function readStore(directory) {
|
|
1655
1509
|
const p = failuresPath(directory);
|
|
1656
|
-
if (!
|
|
1510
|
+
if (!existsSync12(p))
|
|
1657
1511
|
return { version: "1.0", last_updated: new Date().toISOString(), entries: [] };
|
|
1658
1512
|
try {
|
|
1659
|
-
return JSON.parse(
|
|
1513
|
+
return JSON.parse(readFileSync12(p, "utf-8"));
|
|
1660
1514
|
} catch {
|
|
1661
1515
|
return { version: "1.0", last_updated: new Date().toISOString(), entries: [] };
|
|
1662
1516
|
}
|
|
1663
1517
|
}
|
|
1664
1518
|
function writeStore(directory, store) {
|
|
1665
1519
|
const base = codebaseDir(directory);
|
|
1666
|
-
if (!
|
|
1520
|
+
if (!existsSync12(base))
|
|
1667
1521
|
mkdirSync7(base, { recursive: true });
|
|
1668
1522
|
store.last_updated = new Date().toISOString();
|
|
1669
|
-
|
|
1523
|
+
writeFileSync8(failuresPath(directory), JSON.stringify(store, null, 2), "utf-8");
|
|
1670
1524
|
}
|
|
1671
|
-
var failureReplayTool =
|
|
1525
|
+
var failureReplayTool = tool6({
|
|
1672
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",
|
|
1673
1527
|
args: {
|
|
1674
|
-
action:
|
|
1675
|
-
entry:
|
|
1676
|
-
id:
|
|
1677
|
-
type:
|
|
1678
|
-
description:
|
|
1679
|
-
affected_paths:
|
|
1680
|
-
root_cause:
|
|
1681
|
-
fix_applied:
|
|
1682
|
-
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())
|
|
1683
1537
|
}).optional(),
|
|
1684
|
-
query:
|
|
1685
|
-
type:
|
|
1686
|
-
path_prefix:
|
|
1687
|
-
tag:
|
|
1688
|
-
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()
|
|
1689
1543
|
}).optional(),
|
|
1690
|
-
entry_id:
|
|
1544
|
+
entry_id: tool6.schema.string().optional()
|
|
1691
1545
|
},
|
|
1692
1546
|
async execute(args, context) {
|
|
1693
1547
|
const dir = context.directory ?? process.cwd();
|
|
@@ -1749,18 +1603,18 @@ var failureReplayTool = tool7({
|
|
|
1749
1603
|
});
|
|
1750
1604
|
|
|
1751
1605
|
// src/tools/decision-trace.ts
|
|
1752
|
-
import { tool as
|
|
1753
|
-
import { readFileSync as
|
|
1754
|
-
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";
|
|
1755
1609
|
var DECISIONS_FILE = "DECISIONS.jsonl";
|
|
1756
1610
|
function decisionsPath(directory) {
|
|
1757
|
-
return
|
|
1611
|
+
return join12(codebaseDir(directory), DECISIONS_FILE);
|
|
1758
1612
|
}
|
|
1759
1613
|
function readDecisions(directory) {
|
|
1760
1614
|
const p = decisionsPath(directory);
|
|
1761
|
-
if (!
|
|
1615
|
+
if (!existsSync13(p))
|
|
1762
1616
|
return [];
|
|
1763
|
-
return
|
|
1617
|
+
return readFileSync13(p, "utf-8").split(`
|
|
1764
1618
|
`).filter((l) => l.trim()).map((l) => {
|
|
1765
1619
|
try {
|
|
1766
1620
|
return JSON.parse(l);
|
|
@@ -1769,29 +1623,29 @@ function readDecisions(directory) {
|
|
|
1769
1623
|
}
|
|
1770
1624
|
}).filter(Boolean);
|
|
1771
1625
|
}
|
|
1772
|
-
var decisionTraceTool =
|
|
1626
|
+
var decisionTraceTool = tool7({
|
|
1773
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.",
|
|
1774
1628
|
args: {
|
|
1775
|
-
action:
|
|
1776
|
-
entry:
|
|
1777
|
-
id:
|
|
1778
|
-
file_path:
|
|
1779
|
-
change_type:
|
|
1780
|
-
rationale:
|
|
1781
|
-
evidence:
|
|
1782
|
-
assumptions:
|
|
1783
|
-
alternatives_considered:
|
|
1784
|
-
risk_level:
|
|
1785
|
-
agent:
|
|
1786
|
-
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()
|
|
1787
1641
|
}).optional(),
|
|
1788
|
-
query:
|
|
1789
|
-
file_path:
|
|
1790
|
-
change_type:
|
|
1791
|
-
risk_level:
|
|
1792
|
-
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()
|
|
1793
1647
|
}).optional(),
|
|
1794
|
-
file_path:
|
|
1648
|
+
file_path: tool7.schema.string().optional()
|
|
1795
1649
|
},
|
|
1796
1650
|
async execute(args, context) {
|
|
1797
1651
|
const dir = context.directory ?? process.cwd();
|
|
@@ -1800,7 +1654,7 @@ var decisionTraceTool = tool8({
|
|
|
1800
1654
|
case "record": {
|
|
1801
1655
|
if (!args.entry)
|
|
1802
1656
|
return JSON.stringify({ error: "entry required" });
|
|
1803
|
-
if (!
|
|
1657
|
+
if (!existsSync13(base))
|
|
1804
1658
|
mkdirSync8(base, { recursive: true });
|
|
1805
1659
|
const entry = { ...args.entry, timestamp: new Date().toISOString() };
|
|
1806
1660
|
appendFileSync2(decisionsPath(dir), JSON.stringify(entry) + `
|
|
@@ -1833,162 +1687,54 @@ var decisionTraceTool = tool8({
|
|
|
1833
1687
|
}
|
|
1834
1688
|
});
|
|
1835
1689
|
|
|
1836
|
-
// src/tools/volatility-map.ts
|
|
1837
|
-
import { tool as tool9 } from "@opencode-ai/plugin";
|
|
1838
|
-
import { readFileSync as readFileSync15, writeFileSync as writeFileSync11, existsSync as existsSync15, mkdirSync as mkdirSync9 } from "fs";
|
|
1839
|
-
import { join as join14 } from "path";
|
|
1840
|
-
var VOLATILITY_FILE = "VOLATILITY.json";
|
|
1841
|
-
function volatilityPath(directory) {
|
|
1842
|
-
return join14(codebaseDir(directory), VOLATILITY_FILE);
|
|
1843
|
-
}
|
|
1844
|
-
function readStore2(directory) {
|
|
1845
|
-
const p = volatilityPath(directory);
|
|
1846
|
-
if (!existsSync15(p))
|
|
1847
|
-
return { version: "1.0", last_updated: new Date().toISOString(), generated_at: new Date().toISOString(), entries: [] };
|
|
1848
|
-
try {
|
|
1849
|
-
return JSON.parse(readFileSync15(p, "utf-8"));
|
|
1850
|
-
} catch {
|
|
1851
|
-
return { version: "1.0", last_updated: new Date().toISOString(), generated_at: new Date().toISOString(), entries: [] };
|
|
1852
|
-
}
|
|
1853
|
-
}
|
|
1854
|
-
function writeStore2(directory, store) {
|
|
1855
|
-
const base = codebaseDir(directory);
|
|
1856
|
-
if (!existsSync15(base))
|
|
1857
|
-
mkdirSync9(base, { recursive: true });
|
|
1858
|
-
store.last_updated = new Date().toISOString();
|
|
1859
|
-
writeFileSync11(volatilityPath(directory), JSON.stringify(store, null, 2), "utf-8");
|
|
1860
|
-
}
|
|
1861
|
-
function stabilityLabel(churn, hotfixes, todos) {
|
|
1862
|
-
const score = churn + hotfixes * 10 + todos * 2;
|
|
1863
|
-
if (score >= 80)
|
|
1864
|
-
return "critical";
|
|
1865
|
-
if (score >= 50)
|
|
1866
|
-
return "volatile";
|
|
1867
|
-
if (score >= 20)
|
|
1868
|
-
return "moderate";
|
|
1869
|
-
return "stable";
|
|
1870
|
-
}
|
|
1871
|
-
var volatilityMapTool = tool9({
|
|
1872
|
-
description: "Codebase Volatility Map: read/write/query .codebase/VOLATILITY.json — highlights unstable zones based on churn, hotfix frequency, and TODO clusters",
|
|
1873
|
-
args: {
|
|
1874
|
-
action: tool9.schema.enum(["read", "write", "query_hotspots", "update_entry"]),
|
|
1875
|
-
entries: tool9.schema.array(tool9.schema.object({
|
|
1876
|
-
path: tool9.schema.string(),
|
|
1877
|
-
churn_score: tool9.schema.number(),
|
|
1878
|
-
hotfix_count: tool9.schema.number(),
|
|
1879
|
-
todo_count: tool9.schema.number(),
|
|
1880
|
-
last_breakage: tool9.schema.string().optional(),
|
|
1881
|
-
notes: tool9.schema.array(tool9.schema.string())
|
|
1882
|
-
})).optional(),
|
|
1883
|
-
entry: tool9.schema.object({
|
|
1884
|
-
path: tool9.schema.string(),
|
|
1885
|
-
churn_score: tool9.schema.number(),
|
|
1886
|
-
hotfix_count: tool9.schema.number(),
|
|
1887
|
-
todo_count: tool9.schema.number(),
|
|
1888
|
-
last_breakage: tool9.schema.string().optional(),
|
|
1889
|
-
notes: tool9.schema.array(tool9.schema.string())
|
|
1890
|
-
}).optional(),
|
|
1891
|
-
threshold: tool9.schema.enum(["stable", "moderate", "volatile", "critical"]).optional(),
|
|
1892
|
-
path_prefix: tool9.schema.string().optional(),
|
|
1893
|
-
limit: tool9.schema.number().optional()
|
|
1894
|
-
},
|
|
1895
|
-
async execute(args, context) {
|
|
1896
|
-
const dir = context.directory ?? process.cwd();
|
|
1897
|
-
const store = readStore2(dir);
|
|
1898
|
-
switch (args.action) {
|
|
1899
|
-
case "read": {
|
|
1900
|
-
return JSON.stringify({ last_updated: store.last_updated, count: store.entries.length, entries: store.entries });
|
|
1901
|
-
}
|
|
1902
|
-
case "write": {
|
|
1903
|
-
if (!args.entries)
|
|
1904
|
-
return JSON.stringify({ error: "entries required" });
|
|
1905
|
-
store.entries = args.entries.map((e) => ({
|
|
1906
|
-
...e,
|
|
1907
|
-
stability: stabilityLabel(e.churn_score, e.hotfix_count, e.todo_count)
|
|
1908
|
-
}));
|
|
1909
|
-
store.generated_at = new Date().toISOString();
|
|
1910
|
-
writeStore2(dir, store);
|
|
1911
|
-
return JSON.stringify({ success: true, count: store.entries.length });
|
|
1912
|
-
}
|
|
1913
|
-
case "update_entry": {
|
|
1914
|
-
if (!args.entry)
|
|
1915
|
-
return JSON.stringify({ error: "entry required" });
|
|
1916
|
-
const idx = store.entries.findIndex((e) => e.path === args.entry.path);
|
|
1917
|
-
const updated = {
|
|
1918
|
-
...args.entry,
|
|
1919
|
-
stability: stabilityLabel(args.entry.churn_score, args.entry.hotfix_count, args.entry.todo_count)
|
|
1920
|
-
};
|
|
1921
|
-
if (idx >= 0) {
|
|
1922
|
-
store.entries[idx] = updated;
|
|
1923
|
-
} else {
|
|
1924
|
-
store.entries.push(updated);
|
|
1925
|
-
}
|
|
1926
|
-
writeStore2(dir, store);
|
|
1927
|
-
return JSON.stringify({ success: true, path: args.entry.path, stability: updated.stability });
|
|
1928
|
-
}
|
|
1929
|
-
case "query_hotspots": {
|
|
1930
|
-
const levels = { stable: 0, moderate: 1, volatile: 2, critical: 3 };
|
|
1931
|
-
const minLevel = levels[args.threshold ?? "volatile"] ?? 2;
|
|
1932
|
-
let results = store.entries.filter((e) => (levels[e.stability] ?? 0) >= minLevel);
|
|
1933
|
-
if (args.path_prefix)
|
|
1934
|
-
results = results.filter((e) => e.path.startsWith(args.path_prefix));
|
|
1935
|
-
results.sort((a, b) => levels[b.stability] - levels[a.stability] || b.churn_score - a.churn_score);
|
|
1936
|
-
if (args.limit)
|
|
1937
|
-
results = results.slice(0, args.limit);
|
|
1938
|
-
return JSON.stringify({ count: results.length, hotspots: results });
|
|
1939
|
-
}
|
|
1940
|
-
}
|
|
1941
|
-
}
|
|
1942
|
-
});
|
|
1943
|
-
|
|
1944
1690
|
// src/tools/policy-engine.ts
|
|
1945
|
-
import { tool as
|
|
1946
|
-
import { readFileSync as
|
|
1947
|
-
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";
|
|
1948
1694
|
var POLICIES_FILE = "POLICIES.json";
|
|
1949
1695
|
function policiesPath(directory) {
|
|
1950
|
-
return
|
|
1696
|
+
return join13(codebaseDir(directory), POLICIES_FILE);
|
|
1951
1697
|
}
|
|
1952
|
-
function
|
|
1698
|
+
function readStore2(directory) {
|
|
1953
1699
|
const p = policiesPath(directory);
|
|
1954
|
-
if (!
|
|
1700
|
+
if (!existsSync14(p))
|
|
1955
1701
|
return { version: "1.0", last_updated: new Date().toISOString(), policies: [] };
|
|
1956
1702
|
try {
|
|
1957
|
-
return JSON.parse(
|
|
1703
|
+
return JSON.parse(readFileSync14(p, "utf-8"));
|
|
1958
1704
|
} catch {
|
|
1959
1705
|
return { version: "1.0", last_updated: new Date().toISOString(), policies: [] };
|
|
1960
1706
|
}
|
|
1961
1707
|
}
|
|
1962
|
-
function
|
|
1708
|
+
function writeStore2(directory, store) {
|
|
1963
1709
|
const base = codebaseDir(directory);
|
|
1964
|
-
if (!
|
|
1965
|
-
|
|
1710
|
+
if (!existsSync14(base))
|
|
1711
|
+
mkdirSync9(base, { recursive: true });
|
|
1966
1712
|
store.last_updated = new Date().toISOString();
|
|
1967
|
-
|
|
1713
|
+
writeFileSync10(policiesPath(directory), JSON.stringify(store, null, 2), "utf-8");
|
|
1968
1714
|
}
|
|
1969
|
-
var policyEngineTool =
|
|
1715
|
+
var policyEngineTool = tool8({
|
|
1970
1716
|
description: "Self-Healing Policy Engine: manage .codebase/POLICIES.json — add, list, query, toggle, and record violations of editing policies learned from past failures",
|
|
1971
1717
|
args: {
|
|
1972
|
-
action:
|
|
1973
|
-
policy:
|
|
1974
|
-
id:
|
|
1975
|
-
name:
|
|
1976
|
-
trigger:
|
|
1977
|
-
rule:
|
|
1978
|
-
source:
|
|
1979
|
-
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()
|
|
1980
1726
|
}).optional(),
|
|
1981
|
-
policy_id:
|
|
1982
|
-
active:
|
|
1983
|
-
query:
|
|
1984
|
-
source:
|
|
1985
|
-
active_only:
|
|
1986
|
-
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()
|
|
1987
1733
|
}).optional()
|
|
1988
1734
|
},
|
|
1989
1735
|
async execute(args, context) {
|
|
1990
1736
|
const dir = context.directory ?? process.cwd();
|
|
1991
|
-
const store =
|
|
1737
|
+
const store = readStore2(dir);
|
|
1992
1738
|
switch (args.action) {
|
|
1993
1739
|
case "list": {
|
|
1994
1740
|
const active = store.policies.filter((p) => p.active);
|
|
@@ -2003,7 +1749,7 @@ var policyEngineTool = tool10({
|
|
|
2003
1749
|
} else {
|
|
2004
1750
|
store.policies.push({ ...args.policy, created_at: new Date().toISOString(), active: true });
|
|
2005
1751
|
}
|
|
2006
|
-
|
|
1752
|
+
writeStore2(dir, store);
|
|
2007
1753
|
return JSON.stringify({ success: true, id: args.policy.id });
|
|
2008
1754
|
}
|
|
2009
1755
|
case "record_violation": {
|
|
@@ -2014,7 +1760,7 @@ var policyEngineTool = tool10({
|
|
|
2014
1760
|
return JSON.stringify({ error: `Policy not found: ${args.policy_id}` });
|
|
2015
1761
|
policy.failure_count++;
|
|
2016
1762
|
policy.last_violated = new Date().toISOString();
|
|
2017
|
-
|
|
1763
|
+
writeStore2(dir, store);
|
|
2018
1764
|
return JSON.stringify({ success: true, policy_id: args.policy_id, failure_count: policy.failure_count });
|
|
2019
1765
|
}
|
|
2020
1766
|
case "toggle": {
|
|
@@ -2024,7 +1770,7 @@ var policyEngineTool = tool10({
|
|
|
2024
1770
|
if (!policy)
|
|
2025
1771
|
return JSON.stringify({ error: `Policy not found: ${args.policy_id}` });
|
|
2026
1772
|
policy.active = args.active !== undefined ? args.active : !policy.active;
|
|
2027
|
-
|
|
1773
|
+
writeStore2(dir, store);
|
|
2028
1774
|
return JSON.stringify({ success: true, policy_id: args.policy_id, active: policy.active });
|
|
2029
1775
|
}
|
|
2030
1776
|
case "query": {
|
|
@@ -2045,22 +1791,22 @@ var policyEngineTool = tool10({
|
|
|
2045
1791
|
});
|
|
2046
1792
|
|
|
2047
1793
|
// src/tools/hash-edit.ts
|
|
2048
|
-
import { tool as
|
|
2049
|
-
import { readFileSync as
|
|
1794
|
+
import { tool as tool9 } from "@opencode-ai/plugin";
|
|
1795
|
+
import { readFileSync as readFileSync15, writeFileSync as writeFileSync11 } from "fs";
|
|
2050
1796
|
import { createHash as createHash2 } from "crypto";
|
|
2051
|
-
var hashEditTool =
|
|
1797
|
+
var hashEditTool = tool9({
|
|
2052
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.",
|
|
2053
1799
|
args: {
|
|
2054
|
-
filePath:
|
|
2055
|
-
targetContent:
|
|
2056
|
-
expectedHash:
|
|
2057
|
-
replacementContent:
|
|
1800
|
+
filePath: tool9.schema.string(),
|
|
1801
|
+
targetContent: tool9.schema.string(),
|
|
1802
|
+
expectedHash: tool9.schema.string().optional(),
|
|
1803
|
+
replacementContent: tool9.schema.string()
|
|
2058
1804
|
},
|
|
2059
1805
|
async execute(args, context) {
|
|
2060
1806
|
const fullPath = args.filePath.startsWith("/") ? args.filePath : `${context.directory}/${args.filePath}`;
|
|
2061
1807
|
let content;
|
|
2062
1808
|
try {
|
|
2063
|
-
content =
|
|
1809
|
+
content = readFileSync15(fullPath, "utf-8");
|
|
2064
1810
|
} catch (e) {
|
|
2065
1811
|
return `Error: Could not read file ${args.filePath}`;
|
|
2066
1812
|
}
|
|
@@ -2074,17 +1820,17 @@ var hashEditTool = tool11({
|
|
|
2074
1820
|
}
|
|
2075
1821
|
}
|
|
2076
1822
|
const newContent = content.replace(args.targetContent, args.replacementContent);
|
|
2077
|
-
|
|
1823
|
+
writeFileSync11(fullPath, newContent, "utf-8");
|
|
2078
1824
|
return `Successfully updated ${args.filePath} using hash-anchored edit.`;
|
|
2079
1825
|
}
|
|
2080
1826
|
});
|
|
2081
1827
|
|
|
2082
1828
|
// src/tools/council.ts
|
|
2083
|
-
import { tool as
|
|
2084
|
-
import { appendFileSync as appendFileSync3, existsSync as
|
|
2085
|
-
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";
|
|
2086
1832
|
import { createHash as createHash3 } from "crypto";
|
|
2087
|
-
import { readFileSync as
|
|
1833
|
+
import { readFileSync as readFileSync16 } from "fs";
|
|
2088
1834
|
var _councilCache = new Map;
|
|
2089
1835
|
var COUNCIL_CACHE_TTL_MS = 20 * 60 * 1000;
|
|
2090
1836
|
function councilCacheKey(task, agents, stateVersion, indexVersion) {
|
|
@@ -2105,20 +1851,20 @@ async function runWithConcurrencyLimit(tasks, limit) {
|
|
|
2105
1851
|
return results;
|
|
2106
1852
|
}
|
|
2107
1853
|
function createCouncilTool(client) {
|
|
2108
|
-
return
|
|
1854
|
+
return tool10({
|
|
2109
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.",
|
|
2110
1856
|
args: {
|
|
2111
|
-
task:
|
|
2112
|
-
agents:
|
|
2113
|
-
force_fresh:
|
|
2114
|
-
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)
|
|
2115
1861
|
},
|
|
2116
1862
|
async execute(args, context) {
|
|
2117
1863
|
const agents = args.agents || ["architect", "reviewer", "backend-coder"];
|
|
2118
1864
|
const concurrencyLimit = Math.max(1, Math.min(5, typeof args.max_concurrency === "number" ? args.max_concurrency : 3));
|
|
2119
1865
|
const index = readCodebaseIndex(context.directory);
|
|
2120
1866
|
const sp = statePath(context.directory);
|
|
2121
|
-
const rawState =
|
|
1867
|
+
const rawState = existsSync15(sp) ? readFileSync16(sp, "utf-8") : "";
|
|
2122
1868
|
const state = rawState ? parseState(rawState) : {};
|
|
2123
1869
|
const stateVersion = typeof state.summaryVersion === "number" ? state.summaryVersion : 0;
|
|
2124
1870
|
const indexVersion = typeof index.summaryVersion === "number" ? index.summaryVersion : 0;
|
|
@@ -2194,117 +1940,18 @@ Please synthesize these results. Identify areas of agreement, resolve conflicts,
|
|
|
2194
1940
|
function persistCouncilResult(directory, payload) {
|
|
2195
1941
|
try {
|
|
2196
1942
|
const base = codebaseDir(directory);
|
|
2197
|
-
if (!
|
|
2198
|
-
|
|
2199
|
-
const path =
|
|
1943
|
+
if (!existsSync15(base))
|
|
1944
|
+
mkdirSync10(base, { recursive: true });
|
|
1945
|
+
const path = join14(base, "COUNCILS.jsonl");
|
|
2200
1946
|
appendFileSync3(path, JSON.stringify(payload) + `
|
|
2201
1947
|
`, "utf-8");
|
|
2202
1948
|
} catch {}
|
|
2203
1949
|
}
|
|
2204
1950
|
|
|
2205
|
-
// src/tools/context-generator.ts
|
|
2206
|
-
import { tool as tool13 } from "@opencode-ai/plugin";
|
|
2207
|
-
import { writeFileSync as writeFileSync14, existsSync as existsSync18, readFileSync as readFileSync19, readdirSync as readdirSync4, statSync as statSync2 } from "fs";
|
|
2208
|
-
import { join as join17 } from "path";
|
|
2209
|
-
var contextGeneratorTool = tool13({
|
|
2210
|
-
description: "Auto-generate or update hierarchical context files (AGENTS.md, CLAUDE.md) throughout the project. These files provide critical grounding for AI agents.",
|
|
2211
|
-
args: {
|
|
2212
|
-
targetDir: tool13.schema.string().optional(),
|
|
2213
|
-
force: tool13.schema.boolean().optional()
|
|
2214
|
-
},
|
|
2215
|
-
async execute(args, context) {
|
|
2216
|
-
const root = context.directory;
|
|
2217
|
-
const target = args.targetDir ? join17(root, args.targetDir) : root;
|
|
2218
|
-
if (!existsSync18(target)) {
|
|
2219
|
-
return `Error: Directory ${target} does not exist.`;
|
|
2220
|
-
}
|
|
2221
|
-
const agentsMdPath = join17(target, "AGENTS.md");
|
|
2222
|
-
if (existsSync18(agentsMdPath) && !args.force) {
|
|
2223
|
-
return `AGENTS.md already exists in ${target}. Use force: true to overwrite.`;
|
|
2224
|
-
}
|
|
2225
|
-
const pkgPath = join17(root, "package.json");
|
|
2226
|
-
let projectName = "Project";
|
|
2227
|
-
let techStack = "Unknown";
|
|
2228
|
-
if (existsSync18(pkgPath)) {
|
|
2229
|
-
try {
|
|
2230
|
-
const pkg = JSON.parse(readFileSync19(pkgPath, "utf-8"));
|
|
2231
|
-
projectName = pkg.name || projectName;
|
|
2232
|
-
techStack = Object.keys(pkg.dependencies || {}).slice(0, 5).join(", ");
|
|
2233
|
-
} catch {}
|
|
2234
|
-
}
|
|
2235
|
-
const content = `# AGENTS.md for ${projectName}
|
|
2236
|
-
|
|
2237
|
-
## Context
|
|
2238
|
-
- **Tech Stack**: ${techStack}
|
|
2239
|
-
- **Primary Goal**: [Explain the main purpose of this directory/project]
|
|
2240
|
-
|
|
2241
|
-
## Rules for Agents
|
|
2242
|
-
1. **Consistency**: Follow existing patterns in this directory.
|
|
2243
|
-
2. **Safety**: Do not modify files in \`node_modules\` or other sensitive areas.
|
|
2244
|
-
3. **Planning**: Always check \`.planning/STATE.md\` before executing major changes.
|
|
2245
|
-
|
|
2246
|
-
## Directory Map
|
|
2247
|
-
${readdirSync4(target).slice(0, 10).map((f) => {
|
|
2248
|
-
const s = statSync2(join17(target, f));
|
|
2249
|
-
return `- \`${f}\`${s.isDirectory() ? "/" : ""} : [Description]`;
|
|
2250
|
-
}).join(`
|
|
2251
|
-
`)}
|
|
2252
|
-
|
|
2253
|
-
---
|
|
2254
|
-
Generated by FlowDeck Context Generator.
|
|
2255
|
-
`;
|
|
2256
|
-
writeFileSync14(agentsMdPath, content, "utf-8");
|
|
2257
|
-
return `Successfully generated AGENTS.md in ${target}.`;
|
|
2258
|
-
}
|
|
2259
|
-
});
|
|
2260
|
-
|
|
2261
|
-
// src/tools/create-skill.ts
|
|
2262
|
-
import { tool as tool14 } from "@opencode-ai/plugin";
|
|
2263
|
-
import { mkdirSync as mkdirSync12, writeFileSync as writeFileSync15, existsSync as existsSync19 } from "fs";
|
|
2264
|
-
import { join as join18, dirname as dirname3 } from "path";
|
|
2265
|
-
import { fileURLToPath } from "url";
|
|
2266
|
-
var SKILLS_DIR = join18(dirname3(fileURLToPath(import.meta.url)), "..", "skills");
|
|
2267
|
-
var createSkillTool = tool14({
|
|
2268
|
-
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.",
|
|
2269
|
-
args: {
|
|
2270
|
-
name: tool14.schema.string().describe("Unique kebab-case skill name, e.g. 'api-rate-limiting'"),
|
|
2271
|
-
description: tool14.schema.string().describe("One-sentence description of what this skill does"),
|
|
2272
|
-
content: tool14.schema.string().describe("Full skill body in Markdown. Must include: ## When to Activate, ## Steps, and ## Examples sections."),
|
|
2273
|
-
tags: tool14.schema.array(tool14.schema.string()).optional().describe("Optional tags for categorisation, e.g. ['performance', 'typescript']")
|
|
2274
|
-
},
|
|
2275
|
-
async execute(args) {
|
|
2276
|
-
const skillDir = join18(SKILLS_DIR, args.name);
|
|
2277
|
-
const skillFile = join18(skillDir, "SKILL.md");
|
|
2278
|
-
if (existsSync19(skillFile)) {
|
|
2279
|
-
return `Skill '${args.name}' already exists at ${skillFile}.
|
|
2280
|
-
` + `Use a different name or delete the existing skill directory first.`;
|
|
2281
|
-
}
|
|
2282
|
-
const tagLine = args.tags?.length ? `
|
|
2283
|
-
tags: [${args.tags.join(", ")}]` : "";
|
|
2284
|
-
const frontmatter = `---
|
|
2285
|
-
name: ${args.name}
|
|
2286
|
-
description: ${args.description}
|
|
2287
|
-
origin: FlowDeck (self-learned)${tagLine}
|
|
2288
|
-
---
|
|
2289
|
-
|
|
2290
|
-
`;
|
|
2291
|
-
const fullContent = frontmatter + args.content.trimStart();
|
|
2292
|
-
try {
|
|
2293
|
-
mkdirSync12(skillDir, { recursive: true });
|
|
2294
|
-
writeFileSync15(skillFile, fullContent, "utf-8");
|
|
2295
|
-
return `✓ Skill '${args.name}' created at ${skillFile}
|
|
2296
|
-
|
|
2297
|
-
` + `The skill is now part of the FlowDeck library. Restart OpenCode to load it into the active session.`;
|
|
2298
|
-
} catch (err) {
|
|
2299
|
-
return `Error creating skill '${args.name}': ${err.message}`;
|
|
2300
|
-
}
|
|
2301
|
-
}
|
|
2302
|
-
});
|
|
2303
|
-
|
|
2304
1951
|
// src/tools/reflect.ts
|
|
2305
|
-
import { tool as
|
|
2306
|
-
import { existsSync as
|
|
2307
|
-
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";
|
|
2308
1955
|
var MAX_ARTIFACT_BYTES = 4000;
|
|
2309
1956
|
function tail(text, maxBytes) {
|
|
2310
1957
|
if (text.length <= maxBytes)
|
|
@@ -2312,10 +1959,10 @@ function tail(text, maxBytes) {
|
|
|
2312
1959
|
return `... (truncated) ...
|
|
2313
1960
|
` + text.slice(-maxBytes);
|
|
2314
1961
|
}
|
|
2315
|
-
var reflectTool =
|
|
1962
|
+
var reflectTool = tool11({
|
|
2316
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.",
|
|
2317
1964
|
args: {
|
|
2318
|
-
scope:
|
|
1965
|
+
scope: tool11.schema.enum(["session", "project"]).optional().describe("'session' (default) uses only recent artifacts; 'project' includes all historical data")
|
|
2319
1966
|
},
|
|
2320
1967
|
async execute(args, context) {
|
|
2321
1968
|
const root = context.directory;
|
|
@@ -2333,11 +1980,11 @@ var reflectTool = tool15({
|
|
|
2333
1980
|
];
|
|
2334
1981
|
let found = 0;
|
|
2335
1982
|
for (const [rel, label] of ARTIFACT_PATHS) {
|
|
2336
|
-
const full =
|
|
2337
|
-
if (!
|
|
1983
|
+
const full = join15(root, rel);
|
|
1984
|
+
if (!existsSync16(full))
|
|
2338
1985
|
continue;
|
|
2339
1986
|
try {
|
|
2340
|
-
const raw =
|
|
1987
|
+
const raw = readFileSync17(full, "utf-8").trim();
|
|
2341
1988
|
if (!raw)
|
|
2342
1989
|
continue;
|
|
2343
1990
|
const count = raw.split(`
|
|
@@ -2350,23 +1997,23 @@ var reflectTool = tool15({
|
|
|
2350
1997
|
return `No FlowDeck artifacts found under .codebase/.
|
|
2351
1998
|
` + "Run some tasks first so decisions, telemetry, and failures are recorded.";
|
|
2352
1999
|
}
|
|
2353
|
-
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");
|
|
2354
2001
|
return sections.join(`
|
|
2355
2002
|
`);
|
|
2356
2003
|
}
|
|
2357
2004
|
});
|
|
2358
2005
|
|
|
2359
2006
|
// src/tools/codegraph-tool.ts
|
|
2360
|
-
import { tool as
|
|
2007
|
+
import { tool as tool12 } from "@opencode-ai/plugin";
|
|
2361
2008
|
|
|
2362
2009
|
// src/services/codegraph.ts
|
|
2363
2010
|
import { spawnSync } from "child_process";
|
|
2364
|
-
import { existsSync as
|
|
2365
|
-
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";
|
|
2366
2013
|
var CODEGRAPH_META_FILE = "CODEGRAPH.md";
|
|
2367
2014
|
var MAX_FRESHNESS_MS = 30 * 60 * 1000;
|
|
2368
2015
|
function metaPath(dir) {
|
|
2369
|
-
return
|
|
2016
|
+
return join16(codebaseDir(dir), CODEGRAPH_META_FILE);
|
|
2370
2017
|
}
|
|
2371
2018
|
function isCodegraphInstalled() {
|
|
2372
2019
|
try {
|
|
@@ -2381,11 +2028,11 @@ function isCodegraphInstalled() {
|
|
|
2381
2028
|
}
|
|
2382
2029
|
}
|
|
2383
2030
|
function isCodegraphIndexed(dir) {
|
|
2384
|
-
return
|
|
2031
|
+
return existsSync17(join16(dir, ".codegraph", "codegraph.db"));
|
|
2385
2032
|
}
|
|
2386
2033
|
function readCodegraphMeta(dir) {
|
|
2387
2034
|
const path = metaPath(dir);
|
|
2388
|
-
if (!
|
|
2035
|
+
if (!existsSync17(path)) {
|
|
2389
2036
|
return {
|
|
2390
2037
|
installed: false,
|
|
2391
2038
|
indexed: false,
|
|
@@ -2398,7 +2045,7 @@ function readCodegraphMeta(dir) {
|
|
|
2398
2045
|
};
|
|
2399
2046
|
}
|
|
2400
2047
|
try {
|
|
2401
|
-
const content =
|
|
2048
|
+
const content = readFileSync18(path, "utf-8");
|
|
2402
2049
|
return parseCodegraphMeta(content);
|
|
2403
2050
|
} catch {
|
|
2404
2051
|
return {
|
|
@@ -2465,8 +2112,8 @@ function parseCodegraphMeta(content) {
|
|
|
2465
2112
|
}
|
|
2466
2113
|
function writeCodegraphMeta(dir, meta) {
|
|
2467
2114
|
const base = codebaseDir(dir);
|
|
2468
|
-
if (!
|
|
2469
|
-
|
|
2115
|
+
if (!existsSync17(base))
|
|
2116
|
+
mkdirSync11(base, { recursive: true });
|
|
2470
2117
|
const lines = [
|
|
2471
2118
|
"# Codegraph Metadata",
|
|
2472
2119
|
"",
|
|
@@ -2479,7 +2126,7 @@ function writeCodegraphMeta(dir, meta) {
|
|
|
2479
2126
|
`**installLog:** ${meta.installLog}`,
|
|
2480
2127
|
`**indexLog:** ${meta.indexLog}`
|
|
2481
2128
|
];
|
|
2482
|
-
|
|
2129
|
+
writeFileSync12(metaPath(dir), lines.join(`
|
|
2483
2130
|
`), "utf-8");
|
|
2484
2131
|
}
|
|
2485
2132
|
function isCodegraphFresh(dir, maxAgeMs = MAX_FRESHNESS_MS) {
|
|
@@ -2690,11 +2337,11 @@ function markCodegraphStale(dir) {
|
|
|
2690
2337
|
}
|
|
2691
2338
|
|
|
2692
2339
|
// src/tools/codegraph-tool.ts
|
|
2693
|
-
var codegraphTool =
|
|
2340
|
+
var codegraphTool = tool12({
|
|
2694
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.",
|
|
2695
2342
|
args: {
|
|
2696
|
-
action:
|
|
2697
|
-
agent:
|
|
2343
|
+
action: tool12.schema.enum(["check", "install", "init", "refresh", "status", "mark-stale"]),
|
|
2344
|
+
agent: tool12.schema.string().optional()
|
|
2698
2345
|
},
|
|
2699
2346
|
async execute(args, context) {
|
|
2700
2347
|
const dir = context.directory ?? process.cwd();
|
|
@@ -2783,21 +2430,21 @@ var codegraphTool = tool16({
|
|
|
2783
2430
|
});
|
|
2784
2431
|
|
|
2785
2432
|
// src/tools/load-rules.ts
|
|
2786
|
-
import { tool as
|
|
2787
|
-
import { existsSync as
|
|
2788
|
-
import { join as
|
|
2789
|
-
import { fileURLToPath
|
|
2790
|
-
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");
|
|
2791
2438
|
var _loadedPaths = new Set;
|
|
2792
|
-
var loadRulesTool =
|
|
2439
|
+
var loadRulesTool = tool13({
|
|
2793
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).",
|
|
2794
2441
|
args: {
|
|
2795
|
-
stage:
|
|
2796
|
-
languages:
|
|
2797
|
-
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.")
|
|
2798
2445
|
},
|
|
2799
2446
|
async execute(args) {
|
|
2800
|
-
const rulesDir =
|
|
2447
|
+
const rulesDir = existsSync18(RULES_DIR) ? RULES_DIR : null;
|
|
2801
2448
|
if (!rulesDir) {
|
|
2802
2449
|
return JSON.stringify({
|
|
2803
2450
|
loaded: [],
|
|
@@ -2823,7 +2470,7 @@ var loadRulesTool = tool17({
|
|
|
2823
2470
|
continue;
|
|
2824
2471
|
}
|
|
2825
2472
|
try {
|
|
2826
|
-
const text =
|
|
2473
|
+
const text = readFileSync19(rule.path, "utf-8");
|
|
2827
2474
|
contents.push(`## ${name}
|
|
2828
2475
|
|
|
2829
2476
|
${text}`);
|
|
@@ -2854,11 +2501,11 @@ ${text}`);
|
|
|
2854
2501
|
function ruleShortName(rule) {
|
|
2855
2502
|
return rule.path.replace(RULES_DIR + "/", "").replace(/\.md$/, "");
|
|
2856
2503
|
}
|
|
2857
|
-
var listRulesTool =
|
|
2504
|
+
var listRulesTool = tool13({
|
|
2858
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.",
|
|
2859
2506
|
args: {},
|
|
2860
2507
|
async execute() {
|
|
2861
|
-
const rulesDir =
|
|
2508
|
+
const rulesDir = existsSync18(RULES_DIR) ? RULES_DIR : null;
|
|
2862
2509
|
if (!rulesDir) {
|
|
2863
2510
|
return JSON.stringify({ rules: [], error: `Rules directory not found at ${RULES_DIR}` });
|
|
2864
2511
|
}
|
|
@@ -2878,13 +2525,13 @@ var listRulesTool = tool17({
|
|
|
2878
2525
|
});
|
|
2879
2526
|
|
|
2880
2527
|
// src/tools/rtk-setup.ts
|
|
2881
|
-
import { tool as
|
|
2528
|
+
import { tool as tool14 } from "@opencode-ai/plugin";
|
|
2882
2529
|
|
|
2883
2530
|
// src/services/rtk-manager.ts
|
|
2884
2531
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
2885
|
-
import { existsSync as
|
|
2532
|
+
import { existsSync as existsSync19 } from "fs";
|
|
2886
2533
|
import { homedir as homedir2 } from "os";
|
|
2887
|
-
import { join as
|
|
2534
|
+
import { join as join18 } from "path";
|
|
2888
2535
|
|
|
2889
2536
|
// src/services/rtk-policy.ts
|
|
2890
2537
|
var SUPPORTED_COMMANDS = new Set([
|
|
@@ -2930,7 +2577,7 @@ var INSTALL_INSTRUCTIONS = [
|
|
|
2930
2577
|
"After installation, call rtk-setup again to verify detection."
|
|
2931
2578
|
].join(`
|
|
2932
2579
|
`);
|
|
2933
|
-
var CANDIDATE_PATHS = [
|
|
2580
|
+
var CANDIDATE_PATHS = [join18(homedir2(), ".local", "bin", "rtk"), "/usr/local/bin/rtk", "/usr/bin/rtk"];
|
|
2934
2581
|
function detectRtk() {
|
|
2935
2582
|
const fromPath = spawnSync2("rtk", ["--version"], { encoding: "utf-8", timeout: 5000 });
|
|
2936
2583
|
if (fromPath.status === 0) {
|
|
@@ -2939,7 +2586,7 @@ function detectRtk() {
|
|
|
2939
2586
|
return { installed: true, binPath: "rtk", version };
|
|
2940
2587
|
}
|
|
2941
2588
|
for (const candidate of CANDIDATE_PATHS) {
|
|
2942
|
-
if (!
|
|
2589
|
+
if (!existsSync19(candidate))
|
|
2943
2590
|
continue;
|
|
2944
2591
|
const result = spawnSync2(candidate, ["--version"], { encoding: "utf-8", timeout: 5000 });
|
|
2945
2592
|
if (result.status === 0) {
|
|
@@ -3017,7 +2664,7 @@ function getRtkStatus(opts) {
|
|
|
3017
2664
|
}
|
|
3018
2665
|
|
|
3019
2666
|
// src/tools/rtk-setup.ts
|
|
3020
|
-
var rtkSetupTool =
|
|
2667
|
+
var rtkSetupTool = tool14({
|
|
3021
2668
|
description: [
|
|
3022
2669
|
"Detect, initialize, and report status of rtk (output compression proxy for CLI commands).",
|
|
3023
2670
|
"rtk reduces noisy CLI output (git, npm, test runners, linters, docker) by 60-90%.",
|
|
@@ -3025,7 +2672,7 @@ var rtkSetupTool = tool18({
|
|
|
3025
2672
|
"When RTK_INSTALLED=true in the environment, use `$RTK_BIN git status` for compressed output."
|
|
3026
2673
|
].join(" "),
|
|
3027
2674
|
args: {
|
|
3028
|
-
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.")
|
|
3029
2676
|
},
|
|
3030
2677
|
async execute(args) {
|
|
3031
2678
|
const action = args.action ?? "status";
|
|
@@ -3065,15 +2712,15 @@ var rtkSetupTool = tool18({
|
|
|
3065
2712
|
});
|
|
3066
2713
|
|
|
3067
2714
|
// src/hooks/guard-rails.ts
|
|
3068
|
-
import { existsSync as
|
|
3069
|
-
import { join as
|
|
2715
|
+
import { existsSync as existsSync20, readFileSync as readFileSync20 } from "fs";
|
|
2716
|
+
import { join as join19 } from "path";
|
|
3070
2717
|
var PLANNING_DIR2 = ".planning";
|
|
3071
2718
|
var CONFIG_FILE = "config.json";
|
|
3072
2719
|
var STATE_FILE2 = "STATE.md";
|
|
3073
2720
|
function resolveExecutionMode(configPath, trustScore, volatility) {
|
|
3074
|
-
if (
|
|
2721
|
+
if (existsSync20(configPath)) {
|
|
3075
2722
|
try {
|
|
3076
|
-
const config = JSON.parse(
|
|
2723
|
+
const config = JSON.parse(readFileSync20(configPath, "utf-8"));
|
|
3077
2724
|
if (config.execution_mode === "review-only")
|
|
3078
2725
|
return "review-only";
|
|
3079
2726
|
if (config.execution_mode === "guarded")
|
|
@@ -3127,22 +2774,22 @@ async function guardRailsHook(ctx, input, _output) {
|
|
|
3127
2774
|
if (!ENABLED)
|
|
3128
2775
|
return;
|
|
3129
2776
|
const dir = ctx.directory;
|
|
3130
|
-
const planningDirPath =
|
|
2777
|
+
const planningDirPath = join19(dir, PLANNING_DIR2);
|
|
3131
2778
|
const codebaseDirectory = codebaseDir(dir);
|
|
3132
|
-
const configPath =
|
|
3133
|
-
const statePath2 =
|
|
2779
|
+
const configPath = join19(planningDirPath, CONFIG_FILE);
|
|
2780
|
+
const statePath2 = join19(planningDirPath, STATE_FILE2);
|
|
3134
2781
|
const workspaceRoot = findWorkspaceRoot(dir);
|
|
3135
2782
|
if (workspaceRoot && dir !== workspaceRoot) {
|
|
3136
2783
|
const config = getWorkspaceConfig(dir);
|
|
3137
|
-
if (config && config.workspace_mode === "shared" && !
|
|
2784
|
+
if (config && config.workspace_mode === "shared" && !existsSync20(planningDirPath)) {
|
|
3138
2785
|
const msg = `No .planning/ in this sub-repo. Switch to workspace root: cd ${workspaceRoot}`;
|
|
3139
2786
|
throw new Error(`[flowdeck] BLOCK: ${msg}`);
|
|
3140
2787
|
}
|
|
3141
2788
|
}
|
|
3142
2789
|
if (input.tool === "write" || input.tool === "edit") {
|
|
3143
|
-
if (!
|
|
2790
|
+
if (!existsSync20(planningDirPath))
|
|
3144
2791
|
return;
|
|
3145
|
-
if (!
|
|
2792
|
+
if (!existsSync20(codebaseDirectory)) {
|
|
3146
2793
|
throw new Error(`[flowdeck] WARNING: .codebase/ not found. Run /fd-map-codebase to map the codebase.`);
|
|
3147
2794
|
}
|
|
3148
2795
|
const execMode = resolveExecutionMode(configPath, null);
|
|
@@ -3198,15 +2845,15 @@ function getDesignGateMessage(dir) {
|
|
|
3198
2845
|
}
|
|
3199
2846
|
function planSuggestsUiHeavy(dir, phase) {
|
|
3200
2847
|
const planPath = phasePlanPath(dir, phase);
|
|
3201
|
-
if (!
|
|
2848
|
+
if (!existsSync20(planPath))
|
|
3202
2849
|
return false;
|
|
3203
|
-
const planContent =
|
|
2850
|
+
const planContent = readFileSync20(planPath, "utf-8");
|
|
3204
2851
|
return isUiHeavyTask(planContent);
|
|
3205
2852
|
}
|
|
3206
2853
|
function effectiveSeverity(configPath, statePath2) {
|
|
3207
|
-
if (
|
|
2854
|
+
if (existsSync20(configPath)) {
|
|
3208
2855
|
try {
|
|
3209
|
-
const configContent =
|
|
2856
|
+
const configContent = readFileSync20(configPath, "utf-8");
|
|
3210
2857
|
const config = JSON.parse(configContent);
|
|
3211
2858
|
if (config.guard_enforcement === "warn")
|
|
3212
2859
|
return "warn";
|
|
@@ -3222,10 +2869,10 @@ function getEffectiveSeverity(configPath, statePath2) {
|
|
|
3222
2869
|
return effectiveSeverity(configPath, statePath2);
|
|
3223
2870
|
}
|
|
3224
2871
|
function getPlanConfirmed(statePath2) {
|
|
3225
|
-
if (!
|
|
2872
|
+
if (!existsSync20(statePath2))
|
|
3226
2873
|
return false;
|
|
3227
2874
|
try {
|
|
3228
|
-
const content =
|
|
2875
|
+
const content = readFileSync20(statePath2, "utf-8");
|
|
3229
2876
|
const match = content.match(/plan_confirmed:\s*(true|false)/i);
|
|
3230
2877
|
return match ? match[1].toLowerCase() === "true" : false;
|
|
3231
2878
|
} catch {
|
|
@@ -3233,32 +2880,32 @@ function getPlanConfirmed(statePath2) {
|
|
|
3233
2880
|
}
|
|
3234
2881
|
}
|
|
3235
2882
|
function getWarningMessage(planningDir2) {
|
|
3236
|
-
if (!
|
|
2883
|
+
if (!existsSync20(join19(planningDir2, STATE_FILE2))) {
|
|
3237
2884
|
return "No STATE.md found. Run /fd-map-codebase then /fd-new-feature to start a feature.";
|
|
3238
2885
|
}
|
|
3239
2886
|
return "Plan not confirmed. Run /fd-plan and confirm to enable execution.";
|
|
3240
2887
|
}
|
|
3241
2888
|
function getBlockMessage(planningDir2) {
|
|
3242
|
-
if (!
|
|
2889
|
+
if (!existsSync20(join19(planningDir2, STATE_FILE2))) {
|
|
3243
2890
|
return "No STATE.md found. Run /fd-map-codebase then /fd-new-feature to start a feature.";
|
|
3244
2891
|
}
|
|
3245
2892
|
return "Plan not confirmed. Run /fd-plan and confirm to enable execution.";
|
|
3246
2893
|
}
|
|
3247
2894
|
|
|
3248
2895
|
// src/hooks/tool-guard.ts
|
|
3249
|
-
import { existsSync as
|
|
3250
|
-
import { join as
|
|
2896
|
+
import { existsSync as existsSync21, readFileSync as readFileSync21 } from "fs";
|
|
2897
|
+
import { join as join20 } from "path";
|
|
3251
2898
|
var IS_ENABLED = () => process.env.FLOWDECK_TOOL_GUARD_ENABLED === "on";
|
|
3252
2899
|
var BLOCKED_PATTERNS = {
|
|
3253
2900
|
read: [".env", ".pem", ".key", ".secret"],
|
|
3254
2901
|
write: ["node_modules"],
|
|
3255
2902
|
bash: ["rm -rf"]
|
|
3256
2903
|
};
|
|
3257
|
-
function isBlocked(
|
|
3258
|
-
const patterns = BLOCKED_PATTERNS[
|
|
2904
|
+
function isBlocked(tool15, args) {
|
|
2905
|
+
const patterns = BLOCKED_PATTERNS[tool15];
|
|
3259
2906
|
if (!patterns)
|
|
3260
2907
|
return null;
|
|
3261
|
-
if (
|
|
2908
|
+
if (tool15 === "bash") {
|
|
3262
2909
|
const cmd = args.command;
|
|
3263
2910
|
if (!cmd)
|
|
3264
2911
|
return null;
|
|
@@ -3269,7 +2916,7 @@ function isBlocked(tool19, args) {
|
|
|
3269
2916
|
}
|
|
3270
2917
|
return null;
|
|
3271
2918
|
}
|
|
3272
|
-
if (
|
|
2919
|
+
if (tool15 === "read") {
|
|
3273
2920
|
const filePath = args.filePath;
|
|
3274
2921
|
if (!filePath)
|
|
3275
2922
|
return null;
|
|
@@ -3280,7 +2927,7 @@ function isBlocked(tool19, args) {
|
|
|
3280
2927
|
}
|
|
3281
2928
|
return null;
|
|
3282
2929
|
}
|
|
3283
|
-
if (
|
|
2930
|
+
if (tool15 === "write") {
|
|
3284
2931
|
const filePath = args.filePath;
|
|
3285
2932
|
if (!filePath)
|
|
3286
2933
|
return null;
|
|
@@ -3294,11 +2941,11 @@ function isBlocked(tool19, args) {
|
|
|
3294
2941
|
return null;
|
|
3295
2942
|
}
|
|
3296
2943
|
function checkArchConstraint(directory, filePath) {
|
|
3297
|
-
const constraintsPath =
|
|
3298
|
-
if (!
|
|
2944
|
+
const constraintsPath = join20(codebaseDir(directory), "CONSTRAINTS.md");
|
|
2945
|
+
if (!existsSync21(constraintsPath))
|
|
3299
2946
|
return null;
|
|
3300
2947
|
try {
|
|
3301
|
-
const content =
|
|
2948
|
+
const content = readFileSync21(constraintsPath, "utf-8");
|
|
3302
2949
|
const match = content.match(/## Forbidden Paths\n([\s\S]*?)(?:\n##|$)/);
|
|
3303
2950
|
if (!match)
|
|
3304
2951
|
return null;
|
|
@@ -3339,9 +2986,9 @@ function isUiDesignApprovalRequired(directory) {
|
|
|
3339
2986
|
return !(state.design_stage === "handoff_complete" && state.design_approved);
|
|
3340
2987
|
}
|
|
3341
2988
|
const planPath = phasePlanPath(directory, state.phase || 1);
|
|
3342
|
-
if (!
|
|
2989
|
+
if (!existsSync21(planPath))
|
|
3343
2990
|
return false;
|
|
3344
|
-
const planContent =
|
|
2991
|
+
const planContent = readFileSync21(planPath, "utf-8");
|
|
3345
2992
|
if (!isUiHeavyTask(planContent))
|
|
3346
2993
|
return false;
|
|
3347
2994
|
return !(state.design_stage === "handoff_complete" && state.design_approved);
|
|
@@ -3370,18 +3017,18 @@ async function toolGuardHook(ctx, input, output) {
|
|
|
3370
3017
|
}
|
|
3371
3018
|
|
|
3372
3019
|
// src/hooks/session-start.ts
|
|
3373
|
-
import { existsSync as
|
|
3020
|
+
import { existsSync as existsSync22, readFileSync as readFileSync22 } from "fs";
|
|
3374
3021
|
async function sessionStartHook(ctx) {
|
|
3375
3022
|
const planningDir2 = ctx.directory + "/.planning";
|
|
3376
3023
|
const codebaseDirectory = codebaseDir(ctx.directory);
|
|
3377
3024
|
const workspaceRoot = findWorkspaceRoot(ctx.directory);
|
|
3378
3025
|
const config = workspaceRoot ? getWorkspaceConfig(ctx.directory) : null;
|
|
3379
|
-
if (!
|
|
3026
|
+
if (!existsSync22(planningDir2)) {
|
|
3380
3027
|
return {
|
|
3381
3028
|
flowdeck_phase: null,
|
|
3382
3029
|
flowdeck_status: "no_plan",
|
|
3383
3030
|
flowdeck_warning: "Run /fd-map-codebase to index the codebase, then /fd-new-feature to start a feature.",
|
|
3384
|
-
flowdeck_has_codebase:
|
|
3031
|
+
flowdeck_has_codebase: existsSync22(codebaseDirectory),
|
|
3385
3032
|
...workspaceRoot && config?.sub_repos ? {
|
|
3386
3033
|
flowdeck_workspace_root: workspaceRoot,
|
|
3387
3034
|
flowdeck_sub_repos: config.sub_repos,
|
|
@@ -3392,7 +3039,7 @@ async function sessionStartHook(ctx) {
|
|
|
3392
3039
|
}
|
|
3393
3040
|
try {
|
|
3394
3041
|
const stateFilePath = statePath(ctx.directory);
|
|
3395
|
-
const content =
|
|
3042
|
+
const content = readFileSync22(stateFilePath, "utf-8");
|
|
3396
3043
|
const state = parseState(content);
|
|
3397
3044
|
const currentPhase = state["current_phase"] || {};
|
|
3398
3045
|
const result = {
|
|
@@ -3400,7 +3047,7 @@ async function sessionStartHook(ctx) {
|
|
|
3400
3047
|
flowdeck_status: currentPhase["status"] ?? null,
|
|
3401
3048
|
flowdeck_steps_pending: currentPhase["steps_pending"] ?? null,
|
|
3402
3049
|
flowdeck_last_action: currentPhase["last_action"] ?? null,
|
|
3403
|
-
flowdeck_has_codebase:
|
|
3050
|
+
flowdeck_has_codebase: existsSync22(codebaseDirectory)
|
|
3404
3051
|
};
|
|
3405
3052
|
if (workspaceRoot && config?.sub_repos && config.sub_repos.length > 0) {
|
|
3406
3053
|
result.flowdeck_workspace_root = workspaceRoot;
|
|
@@ -3415,7 +3062,7 @@ async function sessionStartHook(ctx) {
|
|
|
3415
3062
|
flowdeck_phase: null,
|
|
3416
3063
|
flowdeck_status: "error",
|
|
3417
3064
|
flowdeck_warning: "State file unreadable. Continuing without flowdeck context.",
|
|
3418
|
-
flowdeck_has_codebase:
|
|
3065
|
+
flowdeck_has_codebase: existsSync22(codebaseDirectory)
|
|
3419
3066
|
};
|
|
3420
3067
|
if (workspaceRoot && config?.sub_repos && config.sub_repos.length > 0) {
|
|
3421
3068
|
result.flowdeck_workspace_root = workspaceRoot;
|
|
@@ -3554,13 +3201,13 @@ class NotificationController {
|
|
|
3554
3201
|
return this.lastNotifiedKey;
|
|
3555
3202
|
}
|
|
3556
3203
|
}
|
|
3557
|
-
function notifyPermissionNeeded(
|
|
3558
|
-
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");
|
|
3559
3206
|
}
|
|
3560
3207
|
|
|
3561
3208
|
// src/hooks/patch-trust.ts
|
|
3562
|
-
import { existsSync as
|
|
3563
|
-
import { join as
|
|
3209
|
+
import { existsSync as existsSync23, readFileSync as readFileSync23 } from "fs";
|
|
3210
|
+
import { join as join21 } from "path";
|
|
3564
3211
|
var HIGH_RISK_KEYWORDS = [
|
|
3565
3212
|
"password",
|
|
3566
3213
|
"secret",
|
|
@@ -3581,26 +3228,12 @@ var HIGH_RISK_KEYWORDS = [
|
|
|
3581
3228
|
"root",
|
|
3582
3229
|
"privilege"
|
|
3583
3230
|
];
|
|
3584
|
-
function loadVolatility(directory) {
|
|
3585
|
-
const p = join25(codebaseDir(directory), "VOLATILITY.json");
|
|
3586
|
-
if (!existsSync27(p))
|
|
3587
|
-
return {};
|
|
3588
|
-
try {
|
|
3589
|
-
const data = JSON.parse(readFileSync26(p, "utf-8"));
|
|
3590
|
-
const map = {};
|
|
3591
|
-
for (const entry of data.entries ?? [])
|
|
3592
|
-
map[entry.path] = entry.stability;
|
|
3593
|
-
return map;
|
|
3594
|
-
} catch {
|
|
3595
|
-
return {};
|
|
3596
|
-
}
|
|
3597
|
-
}
|
|
3598
3231
|
function loadFailedPaths(directory) {
|
|
3599
|
-
const p =
|
|
3600
|
-
if (!
|
|
3232
|
+
const p = join21(codebaseDir(directory), "FAILURES.json");
|
|
3233
|
+
if (!existsSync23(p))
|
|
3601
3234
|
return [];
|
|
3602
3235
|
try {
|
|
3603
|
-
const data = JSON.parse(
|
|
3236
|
+
const data = JSON.parse(readFileSync23(p, "utf-8"));
|
|
3604
3237
|
return (data.entries ?? []).flatMap((e) => e.affected_paths ?? []);
|
|
3605
3238
|
} catch {
|
|
3606
3239
|
return [];
|
|
@@ -3609,18 +3242,6 @@ function loadFailedPaths(directory) {
|
|
|
3609
3242
|
function scorePatch(directory, filePath, content) {
|
|
3610
3243
|
let score = 100;
|
|
3611
3244
|
const signals = [];
|
|
3612
|
-
const volatility = loadVolatility(directory);
|
|
3613
|
-
const stability = Object.entries(volatility).find(([path]) => filePath.includes(path))?.[1];
|
|
3614
|
-
if (stability === "critical") {
|
|
3615
|
-
score -= 40;
|
|
3616
|
-
signals.push("file is in critical volatility zone");
|
|
3617
|
-
} else if (stability === "volatile") {
|
|
3618
|
-
score -= 25;
|
|
3619
|
-
signals.push("file is in volatile zone");
|
|
3620
|
-
} else if (stability === "moderate") {
|
|
3621
|
-
score -= 10;
|
|
3622
|
-
signals.push("file has moderate churn");
|
|
3623
|
-
}
|
|
3624
3245
|
const failedPaths = loadFailedPaths(directory);
|
|
3625
3246
|
if (failedPaths.some((p) => filePath.includes(p))) {
|
|
3626
3247
|
score -= 20;
|
|
@@ -3665,8 +3286,8 @@ async function patchTrustHook(ctx, input, output) {
|
|
|
3665
3286
|
}
|
|
3666
3287
|
|
|
3667
3288
|
// src/hooks/decision-trace-hook.ts
|
|
3668
|
-
import { existsSync as
|
|
3669
|
-
import { join as
|
|
3289
|
+
import { existsSync as existsSync24, mkdirSync as mkdirSync12, appendFileSync as appendFileSync4 } from "fs";
|
|
3290
|
+
import { join as join22 } from "path";
|
|
3670
3291
|
async function decisionTraceHook(ctx, input, output) {
|
|
3671
3292
|
if (input.tool !== "write" && input.tool !== "edit")
|
|
3672
3293
|
return;
|
|
@@ -3675,8 +3296,8 @@ async function decisionTraceHook(ctx, input, output) {
|
|
|
3675
3296
|
return;
|
|
3676
3297
|
const base = codebaseDir(ctx.directory);
|
|
3677
3298
|
try {
|
|
3678
|
-
if (!
|
|
3679
|
-
|
|
3299
|
+
if (!existsSync24(base))
|
|
3300
|
+
mkdirSync12(base, { recursive: true });
|
|
3680
3301
|
const entry = {
|
|
3681
3302
|
timestamp: new Date().toISOString(),
|
|
3682
3303
|
file_path: filePath,
|
|
@@ -3688,87 +3309,14 @@ async function decisionTraceHook(ctx, input, output) {
|
|
|
3688
3309
|
risk_level: "unknown",
|
|
3689
3310
|
auto_recorded: true
|
|
3690
3311
|
};
|
|
3691
|
-
appendFileSync4(
|
|
3312
|
+
appendFileSync4(join22(base, "DECISIONS.jsonl"), JSON.stringify(entry) + `
|
|
3692
3313
|
`, "utf-8");
|
|
3693
3314
|
} catch {}
|
|
3694
3315
|
}
|
|
3695
3316
|
|
|
3696
|
-
// src/services/telemetry.ts
|
|
3697
|
-
import { existsSync as existsSync29, readFileSync as readFileSync27, appendFileSync as appendFileSync5, mkdirSync as mkdirSync15 } from "fs";
|
|
3698
|
-
import { join as join27 } from "path";
|
|
3699
|
-
import { randomUUID } from "crypto";
|
|
3700
|
-
function telemetryPath(dir) {
|
|
3701
|
-
return join27(codebaseDir(dir), "TELEMETRY.jsonl");
|
|
3702
|
-
}
|
|
3703
|
-
function appendEvent2(dir, partial) {
|
|
3704
|
-
if (process.env.TELEMETRY_ENABLED !== "true")
|
|
3705
|
-
return null;
|
|
3706
|
-
const cd = codebaseDir(dir);
|
|
3707
|
-
if (!existsSync29(cd))
|
|
3708
|
-
mkdirSync15(cd, { recursive: true });
|
|
3709
|
-
const event = {
|
|
3710
|
-
id: randomUUID(),
|
|
3711
|
-
ts: new Date().toISOString(),
|
|
3712
|
-
...partial
|
|
3713
|
-
};
|
|
3714
|
-
appendFileSync5(telemetryPath(dir), JSON.stringify(event) + `
|
|
3715
|
-
`, "utf-8");
|
|
3716
|
-
return event;
|
|
3717
|
-
}
|
|
3718
|
-
|
|
3719
|
-
// src/hooks/telemetry-hook.ts
|
|
3720
|
-
function resolveIds(toolInput) {
|
|
3721
|
-
const session_id = toolInput.sessionID ?? toolInput.sessionId ?? process.env.OPENCODE_SESSION_ID ?? "session-0";
|
|
3722
|
-
const run_id = toolInput.messageID ?? toolInput.messageId ?? toolInput.runID ?? toolInput.runId ?? process.env.OPENCODE_RUN_ID ?? "run-0";
|
|
3723
|
-
return { session_id, run_id };
|
|
3724
|
-
}
|
|
3725
|
-
function inferStatus(output) {
|
|
3726
|
-
if (output.error)
|
|
3727
|
-
return "error";
|
|
3728
|
-
if (typeof output.output !== "string")
|
|
3729
|
-
return "ok";
|
|
3730
|
-
const text = output.output.trim();
|
|
3731
|
-
if (!text)
|
|
3732
|
-
return "ok";
|
|
3733
|
-
try {
|
|
3734
|
-
const parsed = JSON.parse(text);
|
|
3735
|
-
if (parsed.success === false || parsed.error || parsed.status === "error")
|
|
3736
|
-
return "error";
|
|
3737
|
-
return "ok";
|
|
3738
|
-
} catch {
|
|
3739
|
-
return "ok";
|
|
3740
|
-
}
|
|
3741
|
-
}
|
|
3742
|
-
async function telemetryHook(context, toolInput, output) {
|
|
3743
|
-
const dir = context.directory ?? process.cwd();
|
|
3744
|
-
const tool19 = toolInput.name ?? toolInput.tool ?? "unknown";
|
|
3745
|
-
const ids = resolveIds(toolInput);
|
|
3746
|
-
appendEvent2(dir, {
|
|
3747
|
-
session_id: ids.session_id,
|
|
3748
|
-
run_id: ids.run_id,
|
|
3749
|
-
event: "tool.call",
|
|
3750
|
-
tool: tool19,
|
|
3751
|
-
status: "ok",
|
|
3752
|
-
meta: { parameters: output.args ?? {} }
|
|
3753
|
-
});
|
|
3754
|
-
}
|
|
3755
|
-
async function telemetryAfterHook(context, toolInput, output) {
|
|
3756
|
-
const dir = context.directory ?? process.cwd();
|
|
3757
|
-
const tool19 = toolInput.name ?? toolInput.tool ?? "unknown";
|
|
3758
|
-
const ids = resolveIds(toolInput);
|
|
3759
|
-
const status = inferStatus(output);
|
|
3760
|
-
appendEvent2(dir, {
|
|
3761
|
-
session_id: ids.session_id,
|
|
3762
|
-
run_id: ids.run_id,
|
|
3763
|
-
event: "tool.complete",
|
|
3764
|
-
tool: tool19,
|
|
3765
|
-
status
|
|
3766
|
-
});
|
|
3767
|
-
}
|
|
3768
|
-
|
|
3769
3317
|
// src/services/approval-manager.ts
|
|
3770
|
-
import { existsSync as
|
|
3771
|
-
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";
|
|
3772
3320
|
var APPROVAL_TTL_MS = 30 * 60 * 1000;
|
|
3773
3321
|
var SENSITIVE_PATTERNS = [
|
|
3774
3322
|
/auth/i,
|
|
@@ -3805,14 +3353,14 @@ function isSensitivePath(filePath) {
|
|
|
3805
3353
|
return SENSITIVE_PATTERNS.some((p) => p.test(filePath));
|
|
3806
3354
|
}
|
|
3807
3355
|
function approvalsPath(dir) {
|
|
3808
|
-
return
|
|
3356
|
+
return join23(codebaseDir(dir), "APPROVALS.json");
|
|
3809
3357
|
}
|
|
3810
3358
|
function loadStore2(dir) {
|
|
3811
3359
|
const p = approvalsPath(dir);
|
|
3812
|
-
if (!
|
|
3360
|
+
if (!existsSync25(p))
|
|
3813
3361
|
return { requests: [] };
|
|
3814
3362
|
try {
|
|
3815
|
-
return JSON.parse(
|
|
3363
|
+
return JSON.parse(readFileSync24(p, "utf-8"));
|
|
3816
3364
|
} catch {
|
|
3817
3365
|
return { requests: [] };
|
|
3818
3366
|
}
|
|
@@ -3830,8 +3378,8 @@ async function approvalHook(context, toolInput, output) {
|
|
|
3830
3378
|
if (!ENABLED2)
|
|
3831
3379
|
return;
|
|
3832
3380
|
const dir = context.directory ?? process.cwd();
|
|
3833
|
-
const
|
|
3834
|
-
if (!WRITE_TOOLS.has(
|
|
3381
|
+
const tool15 = toolInput.name ?? toolInput.tool ?? "";
|
|
3382
|
+
if (!WRITE_TOOLS.has(tool15))
|
|
3835
3383
|
return;
|
|
3836
3384
|
const args = output.args ?? {};
|
|
3837
3385
|
const filePath = String(args.path ?? args.file_path ?? args.filename ?? "");
|
|
@@ -3842,20 +3390,305 @@ async function approvalHook(context, toolInput, output) {
|
|
|
3842
3390
|
const approval = checkApproval(dir, filePath, "");
|
|
3843
3391
|
if (approval)
|
|
3844
3392
|
return;
|
|
3845
|
-
appendEvent2(dir, {
|
|
3846
|
-
session_id: process.env.OPENCODE_SESSION_ID ?? "session-0",
|
|
3847
|
-
run_id: process.env.OPENCODE_RUN_ID ?? "run-0",
|
|
3848
|
-
event: "approval.request",
|
|
3849
|
-
tool: tool19,
|
|
3850
|
-
status: "blocked",
|
|
3851
|
-
files: [filePath],
|
|
3852
|
-
meta: { trigger: "sensitive_file", file: filePath }
|
|
3853
|
-
});
|
|
3854
3393
|
throw new Error(`APPROVAL_REQUIRED: "${filePath}" is a sensitive file (auth/payment/secrets/infra).
|
|
3855
3394
|
` + `Risk level: HIGH — manual approval needed before editing.
|
|
3856
3395
|
` + `To proceed: run /fd-guarded-edit --file "${filePath}" to review and approve this change.`);
|
|
3857
3396
|
}
|
|
3858
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
|
+
|
|
3859
3692
|
// src/hooks/context-window-monitor.ts
|
|
3860
3693
|
var CONTEXT_WARNING_THRESHOLD = 0.7;
|
|
3861
3694
|
var DEFAULT_CONTEXT_LIMIT = Number(process.env.FLOWDECK_CONTEXT_LIMIT) || 200000;
|
|
@@ -3907,8 +3740,8 @@ function createContextWindowMonitorHook() {
|
|
|
3907
3740
|
}
|
|
3908
3741
|
|
|
3909
3742
|
// src/hooks/shell-env-hook.ts
|
|
3910
|
-
import { existsSync as
|
|
3911
|
-
import { join as
|
|
3743
|
+
import { existsSync as existsSync27, readFileSync as readFileSync26 } from "fs";
|
|
3744
|
+
import { join as join25 } from "path";
|
|
3912
3745
|
import { createRequire as createRequire2 } from "module";
|
|
3913
3746
|
var _version;
|
|
3914
3747
|
function getVersion() {
|
|
@@ -3944,7 +3777,7 @@ var MARKER_TO_LANG = {
|
|
|
3944
3777
|
};
|
|
3945
3778
|
function detectPackageManager(root) {
|
|
3946
3779
|
for (const [lockfile, pm] of Object.entries(LOCKFILE_TO_PM)) {
|
|
3947
|
-
if (
|
|
3780
|
+
if (existsSync27(join25(root, lockfile)))
|
|
3948
3781
|
return pm;
|
|
3949
3782
|
}
|
|
3950
3783
|
return;
|
|
@@ -3953,7 +3786,7 @@ function detectLanguages(root) {
|
|
|
3953
3786
|
const langs = [];
|
|
3954
3787
|
const seen = new Set;
|
|
3955
3788
|
for (const [marker, lang] of Object.entries(MARKER_TO_LANG)) {
|
|
3956
|
-
if (!seen.has(lang) &&
|
|
3789
|
+
if (!seen.has(lang) && existsSync27(join25(root, marker))) {
|
|
3957
3790
|
langs.push(lang);
|
|
3958
3791
|
seen.add(lang);
|
|
3959
3792
|
}
|
|
@@ -3961,11 +3794,11 @@ function detectLanguages(root) {
|
|
|
3961
3794
|
return langs;
|
|
3962
3795
|
}
|
|
3963
3796
|
function readCurrentPhase(root) {
|
|
3964
|
-
const statePath2 =
|
|
3965
|
-
if (!
|
|
3797
|
+
const statePath2 = join25(root, ".planning", "STATE.md");
|
|
3798
|
+
if (!existsSync27(statePath2))
|
|
3966
3799
|
return;
|
|
3967
3800
|
try {
|
|
3968
|
-
const content =
|
|
3801
|
+
const content = readFileSync26(statePath2, "utf-8");
|
|
3969
3802
|
const match = content.match(/phase:\s*(\S+)/i);
|
|
3970
3803
|
return match?.[1];
|
|
3971
3804
|
} catch {
|
|
@@ -4090,8 +3923,8 @@ function createSessionIdleHook(client, tracker) {
|
|
|
4090
3923
|
}
|
|
4091
3924
|
|
|
4092
3925
|
// src/hooks/compaction-hook.ts
|
|
4093
|
-
import { existsSync as
|
|
4094
|
-
import { join as
|
|
3926
|
+
import { existsSync as existsSync28, readFileSync as readFileSync27 } from "fs";
|
|
3927
|
+
import { join as join26 } from "path";
|
|
4095
3928
|
var STRUCTURED_SUMMARY_PROMPT = `
|
|
4096
3929
|
When summarizing this session, you MUST include the following sections:
|
|
4097
3930
|
|
|
@@ -4132,10 +3965,10 @@ For each: agent name, status, description, session_id.
|
|
|
4132
3965
|
var _lastInjected = new Map;
|
|
4133
3966
|
function readPlanningState2(directory) {
|
|
4134
3967
|
const sp = statePath(directory);
|
|
4135
|
-
if (!
|
|
3968
|
+
if (!existsSync28(sp))
|
|
4136
3969
|
return null;
|
|
4137
3970
|
try {
|
|
4138
|
-
const content =
|
|
3971
|
+
const content = readFileSync27(sp, "utf-8");
|
|
4139
3972
|
const parsed = parseState(content);
|
|
4140
3973
|
const version = typeof parsed.summaryVersion === "number" ? parsed.summaryVersion : 0;
|
|
4141
3974
|
return { content: content.slice(0, 1500), version };
|
|
@@ -4164,15 +3997,15 @@ function createCompactionHook(ctx, tracker) {
|
|
|
4164
3997
|
sections.push(`_State unchanged since last compaction. summaryVersion=${currentStateVersion}_`);
|
|
4165
3998
|
sections.push("");
|
|
4166
3999
|
}
|
|
4167
|
-
const indexPath2 =
|
|
4168
|
-
if (indexChanged &&
|
|
4000
|
+
const indexPath2 = join26(ctx.directory, ".planning", "CODEBASE_INDEX.md");
|
|
4001
|
+
if (indexChanged && existsSync28(indexPath2)) {
|
|
4169
4002
|
try {
|
|
4170
|
-
const indexContent =
|
|
4003
|
+
const indexContent = readFileSync27(indexPath2, "utf-8");
|
|
4171
4004
|
const indexSummary = "\n## Codebase Index\n```\n" + indexContent.slice(0, 800) + "\n```\n";
|
|
4172
4005
|
sections.push(indexSummary);
|
|
4173
4006
|
sections.push("");
|
|
4174
4007
|
} catch {}
|
|
4175
|
-
} else if (
|
|
4008
|
+
} else if (existsSync28(indexPath2)) {
|
|
4176
4009
|
sections.push(`## Codebase Index (unchanged, v${currentIndexVersion})`);
|
|
4177
4010
|
sections.push(`_Index unchanged since last compaction. summaryVersion=${currentIndexVersion}_`);
|
|
4178
4011
|
sections.push("");
|
|
@@ -4223,12 +4056,9 @@ var ALWAYS_ALLOWED = new Set([
|
|
|
4223
4056
|
"council",
|
|
4224
4057
|
"planning-state",
|
|
4225
4058
|
"codebase-state",
|
|
4226
|
-
"workspace-state",
|
|
4227
4059
|
"repo-memory",
|
|
4228
4060
|
"decision-trace",
|
|
4229
4061
|
"policy-engine",
|
|
4230
|
-
"context-generator",
|
|
4231
|
-
"create-skill",
|
|
4232
4062
|
"reflect"
|
|
4233
4063
|
]);
|
|
4234
4064
|
function isDelegationTool(name) {
|
|
@@ -4339,7 +4169,7 @@ async function runAutoLearner(client, directory, appLog) {
|
|
|
4339
4169
|
parts: [
|
|
4340
4170
|
{
|
|
4341
4171
|
type: "text",
|
|
4342
|
-
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."
|
|
4343
4173
|
}
|
|
4344
4174
|
],
|
|
4345
4175
|
tools: { question: false }
|
|
@@ -4573,8 +4403,8 @@ Please advise.
|
|
|
4573
4403
|
## Self-Learning
|
|
4574
4404
|
|
|
4575
4405
|
When a task required unusual human guidance, a novel solution strategy, or exposed a knowledge gap:
|
|
4576
|
-
1. After the task completes successfully,
|
|
4577
|
-
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
|
|
4578
4408
|
3. Include: When to Activate, Steps, Examples, and Pitfalls sections
|
|
4579
4409
|
|
|
4580
4410
|
Do NOT create a skill for routine tasks. Only capture genuinely novel or reusable patterns.`;
|
|
@@ -6720,7 +6550,6 @@ You receive a structured context with:
|
|
|
6720
6550
|
- \`file_path\`: optional specific file being changed
|
|
6721
6551
|
- \`trust_score\`: patch trust score (0–100; 80+ = safe, 40–79 = review-required, <40 = high-risk)
|
|
6722
6552
|
- \`trust_signals\`: list of risk signals from the patch trust scorer
|
|
6723
|
-
- \`volatile_zones\`: paths marked as volatile or critical in VOLATILITY.json
|
|
6724
6553
|
- \`prior_failures\`: failure entries from FAILURES.json that match this change
|
|
6725
6554
|
- \`regression_categories\`: predicted regression categories for this change
|
|
6726
6555
|
- \`confidence\`: system confidence score (0–100; based on how much codebase context data exists)
|
|
@@ -7179,7 +7008,7 @@ var AUTO_LEARNER_PROMPT = `You run automatically after a coding session to captu
|
|
|
7179
7008
|
- Novel solutions that took non-obvious reasoning
|
|
7180
7009
|
- Recurring tool sequences that indicate a reusable workflow
|
|
7181
7010
|
- Knowledge gaps that had to be worked out from scratch
|
|
7182
|
-
3. For each valuable pattern,
|
|
7011
|
+
3. For each valuable pattern, write a skill markdown file under \`src/skills/<name>/SKILL.md\` immediately.
|
|
7183
7012
|
4. If nothing is worth capturing, output exactly: "No new skills identified."
|
|
7184
7013
|
5. End with a one-line summary: "Auto-learn complete: N skill(s) created."
|
|
7185
7014
|
|
|
@@ -7544,12 +7373,9 @@ var CONTRACTS = [
|
|
|
7544
7373
|
"council",
|
|
7545
7374
|
"planning-state",
|
|
7546
7375
|
"codebase-state",
|
|
7547
|
-
"workspace-state",
|
|
7548
7376
|
"repo-memory",
|
|
7549
7377
|
"decision-trace",
|
|
7550
7378
|
"policy-engine",
|
|
7551
|
-
"context-generator",
|
|
7552
|
-
"create-skill",
|
|
7553
7379
|
"reflect"
|
|
7554
7380
|
],
|
|
7555
7381
|
forbiddenActions: [
|
|
@@ -7584,7 +7410,7 @@ var CONTRACTS = [
|
|
|
7584
7410
|
allowedTaskTypes: ["planning", "task-breakdown", "step-decomposition"],
|
|
7585
7411
|
requiredInputs: ["task description or STATE.md"],
|
|
7586
7412
|
expectedOutputFields: ["steps", "phase"],
|
|
7587
|
-
allowedTools: ["read", "glob", "grep", "planning-state"
|
|
7413
|
+
allowedTools: ["read", "glob", "grep", "planning-state"],
|
|
7588
7414
|
forbiddenActions: [
|
|
7589
7415
|
"write source files",
|
|
7590
7416
|
"run bash commands",
|
|
@@ -8100,7 +7926,6 @@ function runSupervisorReview(directory, targetName, ctx = {}, clarificationQuest
|
|
|
8100
7926
|
reviewPhase,
|
|
8101
7927
|
timestamp: timestamp2
|
|
8102
7928
|
};
|
|
8103
|
-
_emitTelemetry(directory, decision2, ctx);
|
|
8104
7929
|
return decision2;
|
|
8105
7930
|
}
|
|
8106
7931
|
const policyResult = targetType === "command" ? checkCommandPolicy(targetName, ctx) : checkAgentPolicy(targetName, ctx);
|
|
@@ -8122,7 +7947,6 @@ function runSupervisorReview(directory, targetName, ctx = {}, clarificationQuest
|
|
|
8122
7947
|
timestamp: timestamp2,
|
|
8123
7948
|
...escalationQuestion ? { clarificationQuestion: escalationQuestion } : {}
|
|
8124
7949
|
};
|
|
8125
|
-
_emitTelemetry(directory, supervisorDecision, ctx);
|
|
8126
7950
|
return supervisorDecision;
|
|
8127
7951
|
}
|
|
8128
7952
|
function shouldProceed(decision, mode, canBlock) {
|
|
@@ -8135,33 +7959,12 @@ function shouldProceed(decision, mode, canBlock) {
|
|
|
8135
7959
|
}
|
|
8136
7960
|
return decision.decision !== "block" || decision.confidenceScore > 0.3;
|
|
8137
7961
|
}
|
|
8138
|
-
function _emitTelemetry(directory, decision, ctx) {
|
|
8139
|
-
try {
|
|
8140
|
-
appendEvent2(directory, {
|
|
8141
|
-
session_id: ctx.session_id ?? "session-0",
|
|
8142
|
-
run_id: ctx.run_id ?? "unknown",
|
|
8143
|
-
event: "supervisor.review",
|
|
8144
|
-
agent: "supervisor",
|
|
8145
|
-
status: decision.decision === "approve" ? "ok" : decision.decision === "block" ? "blocked" : decision.decision === "escalate" ? "approved" : "ok",
|
|
8146
|
-
meta: {
|
|
8147
|
-
targetName: decision.targetName,
|
|
8148
|
-
targetType: decision.targetType,
|
|
8149
|
-
exists: decision.exists,
|
|
8150
|
-
decision: decision.decision,
|
|
8151
|
-
confidenceScore: decision.confidenceScore,
|
|
8152
|
-
riskFlags: decision.riskFlags,
|
|
8153
|
-
missingRequirements: decision.missingRequirements,
|
|
8154
|
-
reviewPhase: decision.reviewPhase
|
|
8155
|
-
}
|
|
8156
|
-
});
|
|
8157
|
-
} catch {}
|
|
8158
|
-
}
|
|
8159
7962
|
|
|
8160
7963
|
// src/index.ts
|
|
8161
7964
|
function lazyLoadRulePaths(projectRoot) {
|
|
8162
|
-
const __dir =
|
|
8163
|
-
const rulesDir =
|
|
8164
|
-
if (!
|
|
7965
|
+
const __dir = dirname3(fileURLToPath2(import.meta.url));
|
|
7966
|
+
const rulesDir = join27(__dir, "..", "src", "rules");
|
|
7967
|
+
if (!existsSync29(rulesDir))
|
|
8165
7968
|
return { paths: [], diagnostics: "[LazyRuleLoader] rules directory not found" };
|
|
8166
7969
|
const detectedLanguages = detectProjectLanguages(projectRoot);
|
|
8167
7970
|
const paths = getStartupRulePaths(rulesDir, detectedLanguages);
|
|
@@ -8170,17 +7973,17 @@ function lazyLoadRulePaths(projectRoot) {
|
|
|
8170
7973
|
return { paths, diagnostics };
|
|
8171
7974
|
}
|
|
8172
7975
|
function loadCommands() {
|
|
8173
|
-
const __dir =
|
|
8174
|
-
const commandsDir =
|
|
8175
|
-
if (!
|
|
7976
|
+
const __dir = dirname3(fileURLToPath2(import.meta.url));
|
|
7977
|
+
const commandsDir = join27(__dir, "..", "src", "commands");
|
|
7978
|
+
if (!existsSync29(commandsDir))
|
|
8176
7979
|
return {};
|
|
8177
7980
|
const commands = {};
|
|
8178
7981
|
try {
|
|
8179
|
-
for (const file of
|
|
7982
|
+
for (const file of readdirSync4(commandsDir)) {
|
|
8180
7983
|
if (!file.endsWith(".md"))
|
|
8181
7984
|
continue;
|
|
8182
7985
|
const name = basename2(file, ".md");
|
|
8183
|
-
const raw =
|
|
7986
|
+
const raw = readFileSync28(join27(commandsDir, file), "utf-8");
|
|
8184
7987
|
let description;
|
|
8185
7988
|
let template = raw;
|
|
8186
7989
|
const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
|
|
@@ -8197,6 +8000,7 @@ function loadCommands() {
|
|
|
8197
8000
|
}
|
|
8198
8001
|
var plugin = async (input, _options) => {
|
|
8199
8002
|
const { directory, client, worktree } = input;
|
|
8003
|
+
const appLog = (msg) => client.app.log({ body: { service: "flowdeck", level: "info", message: msg } }).catch(() => {});
|
|
8200
8004
|
const runPipelineTool = createRunPipelineTool(client);
|
|
8201
8005
|
const delegateTool = createDelegateTool(client);
|
|
8202
8006
|
const councilTool = createCouncilTool(client);
|
|
@@ -8208,11 +8012,11 @@ var plugin = async (input, _options) => {
|
|
|
8208
8012
|
const sessionIdleHook = createSessionIdleHook(client, fileTracker);
|
|
8209
8013
|
const compactionHook = createCompactionHook({ directory }, fileTracker);
|
|
8210
8014
|
const orchestratorGuard = new OrchestratorGuard;
|
|
8211
|
-
const appLog = (msg) => client.app.log({ body: { service: "flowdeck", level: "info", message: msg } }).catch(() => {});
|
|
8212
8015
|
const autoLearnHook = createAutoLearnHook(client, fileTracker, directory, appLog);
|
|
8213
8016
|
const notifCtrl = new NotificationController(undefined, appLog);
|
|
8214
8017
|
const agentConfigs = getAgentConfigs({});
|
|
8215
8018
|
const mcps = createFlowDeckMcps();
|
|
8019
|
+
let lastExecutedCommand = null;
|
|
8216
8020
|
return {
|
|
8217
8021
|
name: "@dv.nghiem/flowdeck",
|
|
8218
8022
|
agent: agentConfigs,
|
|
@@ -8262,8 +8066,8 @@ var plugin = async (input, _options) => {
|
|
|
8262
8066
|
}
|
|
8263
8067
|
}
|
|
8264
8068
|
}
|
|
8265
|
-
const skillsDir =
|
|
8266
|
-
if (
|
|
8069
|
+
const skillsDir = join27(dirname3(fileURLToPath2(import.meta.url)), "..", "src", "skills");
|
|
8070
|
+
if (existsSync29(skillsDir)) {
|
|
8267
8071
|
const cfgAny = cfg;
|
|
8268
8072
|
if (!cfgAny.skills || typeof cfgAny.skills !== "object") {
|
|
8269
8073
|
cfgAny.skills = { paths: [] };
|
|
@@ -8292,18 +8096,14 @@ var plugin = async (input, _options) => {
|
|
|
8292
8096
|
tool: {
|
|
8293
8097
|
"planning-state": planningStateTool,
|
|
8294
8098
|
"codebase-state": codebaseStateTool,
|
|
8295
|
-
"workspace-state": workspaceStateTool,
|
|
8296
8099
|
"run-pipeline": runPipelineTool,
|
|
8297
8100
|
delegate: delegateTool,
|
|
8298
8101
|
"repo-memory": repoMemoryTool,
|
|
8299
8102
|
"failure-replay": failureReplayTool,
|
|
8300
8103
|
"decision-trace": decisionTraceTool,
|
|
8301
|
-
"volatility-map": volatilityMapTool,
|
|
8302
8104
|
"policy-engine": policyEngineTool,
|
|
8303
8105
|
"hash-edit": hashEditTool,
|
|
8304
8106
|
council: councilTool,
|
|
8305
|
-
"context-generator": contextGeneratorTool,
|
|
8306
|
-
"create-skill": createSkillTool,
|
|
8307
8107
|
reflect: reflectTool,
|
|
8308
8108
|
codegraph: codegraphTool,
|
|
8309
8109
|
"load-rules": loadRulesTool,
|
|
@@ -8315,7 +8115,9 @@ var plugin = async (input, _options) => {
|
|
|
8315
8115
|
"file.edited": fileEdited,
|
|
8316
8116
|
"file.watcher.updated": fileWatcherUpdated,
|
|
8317
8117
|
"experimental.session.compacting": compactionHook,
|
|
8318
|
-
"command.execute.before": async (
|
|
8118
|
+
"command.execute.before": async (input2, _output) => {
|
|
8119
|
+
lastExecutedCommand = input2.command;
|
|
8120
|
+
},
|
|
8319
8121
|
"permission.ask": async (input2, _output) => {
|
|
8320
8122
|
notifyPermissionNeeded(input2.title);
|
|
8321
8123
|
},
|
|
@@ -8323,6 +8125,9 @@ var plugin = async (input, _options) => {
|
|
|
8323
8125
|
const type = event?.type ?? "";
|
|
8324
8126
|
if (type === "session.created" || type === "session.started") {
|
|
8325
8127
|
await sessionStartHook({ directory });
|
|
8128
|
+
if (type === "session.created") {
|
|
8129
|
+
await eventLogSessionHook({ directory }, event);
|
|
8130
|
+
}
|
|
8326
8131
|
}
|
|
8327
8132
|
if (type === "command.executed") {
|
|
8328
8133
|
const commandName = event?.properties?.name ?? "";
|
|
@@ -8333,7 +8138,11 @@ var plugin = async (input, _options) => {
|
|
|
8333
8138
|
await contextMonitor.event({ event });
|
|
8334
8139
|
orchestratorGuard.onEvent(event);
|
|
8335
8140
|
if (type === "session.idle") {
|
|
8141
|
+
await eventLogSessionHook({ directory }, event);
|
|
8336
8142
|
const hasEdits = fileTracker.getEditedPaths().length > 0;
|
|
8143
|
+
if (lastExecutedCommand) {
|
|
8144
|
+
lastExecutedCommand = null;
|
|
8145
|
+
}
|
|
8337
8146
|
notifCtrl.onSessionIdle(hasEdits);
|
|
8338
8147
|
try {
|
|
8339
8148
|
await sessionIdleHook();
|
|
@@ -8343,6 +8152,8 @@ var plugin = async (input, _options) => {
|
|
|
8343
8152
|
}
|
|
8344
8153
|
}
|
|
8345
8154
|
if (type === "session.error") {
|
|
8155
|
+
await eventLogSessionHook({ directory }, event);
|
|
8156
|
+
lastExecutedCommand = null;
|
|
8346
8157
|
const err = event?.properties?.error;
|
|
8347
8158
|
const errorMsg = (err && typeof err === "object" && "message" in err ? String(err.message) : undefined) ?? (typeof err === "string" ? err : undefined) ?? "An unexpected error occurred";
|
|
8348
8159
|
notifCtrl.onSessionError(errorMsg);
|
|
@@ -8387,15 +8198,15 @@ var plugin = async (input, _options) => {
|
|
|
8387
8198
|
}
|
|
8388
8199
|
}
|
|
8389
8200
|
}
|
|
8390
|
-
await telemetryHook({ directory }, toolInput, toolOutput);
|
|
8391
8201
|
await approvalHook({ directory }, toolInput, toolOutput);
|
|
8392
8202
|
await guardRailsHook({ directory }, toolInput, toolOutput);
|
|
8393
8203
|
await toolGuardHook({ directory }, toolInput, toolOutput);
|
|
8394
8204
|
await patchTrustHook({ directory }, toolInput, toolOutput);
|
|
8395
8205
|
await decisionTraceHook({ directory }, toolInput, toolOutput);
|
|
8206
|
+
await eventLogBeforeHook({ directory }, toolInput, toolOutput);
|
|
8396
8207
|
},
|
|
8397
8208
|
"tool.execute.after": async (toolInput, toolOutput) => {
|
|
8398
|
-
await
|
|
8209
|
+
await eventLogAfterHook({ directory }, toolInput, toolOutput);
|
|
8399
8210
|
const afterToolName = toolInput.tool ?? toolInput.name ?? "";
|
|
8400
8211
|
if (afterToolName === "delegate" || afterToolName === "run-pipeline") {
|
|
8401
8212
|
try {
|