@glrs-dev/cli 0.0.1 → 0.1.1
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/CHANGELOG.md +50 -0
- package/README.md +14 -15
- package/dist/chunk-3RG5ZIWI.js +10 -0
- package/dist/chunk-6RHN2EDH.js +93 -0
- package/dist/chunk-DEODG2LC.js +55 -0
- package/dist/chunk-FSAGM22T.js +17 -0
- package/dist/chunk-GQBZREK5.js +136 -0
- package/dist/chunk-HWMRY35D.js +139 -0
- package/dist/chunk-LMRDQ4GW.js +129 -0
- package/dist/chunk-NLPX2KOF.js +149 -0
- package/dist/chunk-P7PRH4I3.js +177 -0
- package/dist/chunk-VCN7RNLU.js +60 -0
- package/dist/chunk-VJFNIKQJ.js +120 -0
- package/dist/chunk-W37UX3U2.js +35 -0
- package/dist/chunk-YBCA3IP6.js +25 -0
- package/dist/chunk-YGNDPKIW.js +99 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +89 -36
- package/dist/commands/cleanup.d.ts +19 -0
- package/dist/commands/cleanup.js +11 -0
- package/dist/commands/create.d.ts +17 -0
- package/dist/commands/create.js +12 -0
- package/dist/commands/delete.d.ts +17 -0
- package/dist/commands/delete.js +12 -0
- package/dist/commands/go.d.ts +4 -0
- package/dist/commands/go.js +11 -0
- package/dist/commands/list.d.ts +15 -0
- package/dist/commands/list.js +12 -0
- package/dist/commands/switch.d.ts +11 -0
- package/dist/commands/switch.js +12 -0
- package/dist/commands/types.d.ts +10 -0
- package/dist/commands/types.js +0 -0
- package/dist/index.d.ts +16 -19
- package/dist/index.js +4 -1
- package/dist/lib/config.d.ts +14 -0
- package/dist/lib/config.js +14 -0
- package/dist/lib/fmt.d.ts +12 -0
- package/dist/lib/fmt.js +25 -0
- package/dist/lib/git.d.ts +26 -0
- package/dist/lib/git.js +25 -0
- package/dist/lib/registry.d.ts +14 -0
- package/dist/lib/registry.js +13 -0
- package/dist/lib/select.d.ts +21 -0
- package/dist/lib/select.js +10 -0
- package/dist/lib/worktree.d.ts +35 -0
- package/dist/lib/worktree.js +17 -0
- package/dist/vendor/harness-opencode/dist/agents/prompts/agents-md-writer.md +89 -0
- package/dist/vendor/harness-opencode/dist/agents/prompts/architecture-advisor.md +46 -0
- package/dist/vendor/harness-opencode/dist/agents/prompts/build.md +93 -0
- package/dist/vendor/harness-opencode/dist/agents/prompts/code-searcher.md +54 -0
- package/dist/vendor/harness-opencode/dist/agents/prompts/docs-maintainer.md +128 -0
- package/dist/vendor/harness-opencode/dist/agents/prompts/gap-analyzer.md +44 -0
- package/dist/vendor/harness-opencode/dist/agents/prompts/lib-reader.md +39 -0
- package/dist/vendor/harness-opencode/dist/agents/prompts/pilot-builder.md +107 -0
- package/dist/vendor/harness-opencode/dist/agents/prompts/pilot-planner.md +153 -0
- package/dist/vendor/harness-opencode/dist/agents/prompts/plan-reviewer.md +49 -0
- package/dist/vendor/harness-opencode/dist/agents/prompts/plan.md +144 -0
- package/dist/vendor/harness-opencode/dist/agents/prompts/prime.md +374 -0
- package/dist/vendor/harness-opencode/dist/agents/prompts/qa-reviewer.md +68 -0
- package/dist/vendor/harness-opencode/dist/agents/prompts/qa-thorough.md +63 -0
- package/dist/vendor/harness-opencode/dist/agents/prompts/research.md +138 -0
- package/dist/vendor/harness-opencode/dist/agents/shared/index.ts +26 -0
- package/dist/vendor/harness-opencode/dist/agents/shared/workflow-mechanics.md +32 -0
- package/dist/vendor/harness-opencode/dist/bin/memory-mcp-launcher.sh +145 -0
- package/dist/vendor/harness-opencode/dist/bin/plan-check.sh +255 -0
- package/dist/vendor/harness-opencode/dist/chunk-VJUETC6A.js +205 -0
- package/dist/vendor/harness-opencode/dist/chunk-VVMP6QWS.js +731 -0
- package/dist/vendor/harness-opencode/dist/chunk-XCZ3NOXR.js +703 -0
- package/dist/vendor/harness-opencode/dist/cli.d.ts +1 -0
- package/dist/vendor/harness-opencode/dist/cli.js +5096 -0
- package/dist/vendor/harness-opencode/dist/commands/prompts/autopilot.md +96 -0
- package/dist/vendor/harness-opencode/dist/commands/prompts/costs.md +94 -0
- package/dist/vendor/harness-opencode/dist/commands/prompts/fresh.md +382 -0
- package/dist/vendor/harness-opencode/dist/commands/prompts/init-deep.md +196 -0
- package/dist/vendor/harness-opencode/dist/commands/prompts/research.md +27 -0
- package/dist/vendor/harness-opencode/dist/commands/prompts/review.md +96 -0
- package/dist/vendor/harness-opencode/dist/commands/prompts/ship.md +104 -0
- package/dist/vendor/harness-opencode/dist/index.d.ts +21 -0
- package/dist/vendor/harness-opencode/dist/index.js +2092 -0
- package/dist/vendor/harness-opencode/dist/install-4EYR56OR.js +9 -0
- package/dist/vendor/harness-opencode/dist/skills/agent-estimation/SKILL.md +159 -0
- package/dist/vendor/harness-opencode/dist/skills/paths.ts +18 -0
- package/dist/vendor/harness-opencode/dist/skills/pilot-planning/SKILL.md +49 -0
- package/dist/vendor/harness-opencode/dist/skills/pilot-planning/rules/dag-shape.md +47 -0
- package/dist/vendor/harness-opencode/dist/skills/pilot-planning/rules/decomposition.md +36 -0
- package/dist/vendor/harness-opencode/dist/skills/pilot-planning/rules/first-principles.md +29 -0
- package/dist/vendor/harness-opencode/dist/skills/pilot-planning/rules/milestones.md +57 -0
- package/dist/vendor/harness-opencode/dist/skills/pilot-planning/rules/self-review.md +46 -0
- package/dist/vendor/harness-opencode/dist/skills/pilot-planning/rules/task-context.md +47 -0
- package/dist/vendor/harness-opencode/dist/skills/pilot-planning/rules/touches-scope.md +47 -0
- package/dist/vendor/harness-opencode/dist/skills/pilot-planning/rules/verify-design.md +53 -0
- package/dist/vendor/harness-opencode/dist/skills/research/SKILL.md +350 -0
- package/dist/vendor/harness-opencode/dist/skills/research-auto/SKILL.md +283 -0
- package/dist/vendor/harness-opencode/dist/skills/research-local/SKILL.md +268 -0
- package/dist/vendor/harness-opencode/dist/skills/research-web/SKILL.md +119 -0
- package/dist/vendor/harness-opencode/dist/skills/review-plan/SKILL.md +32 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-composition-patterns/AGENTS.md +946 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-composition-patterns/README.md +60 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-composition-patterns/SKILL.md +89 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-composition-patterns/rules/architecture-avoid-boolean-props.md +100 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-composition-patterns/rules/architecture-compound-components.md +112 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-composition-patterns/rules/patterns-children-over-render-props.md +87 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-composition-patterns/rules/patterns-explicit-variants.md +100 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-composition-patterns/rules/react19-no-forwardref.md +42 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-composition-patterns/rules/state-context-interface.md +191 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-composition-patterns/rules/state-decouple-implementation.md +113 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-composition-patterns/rules/state-lift-state.md +125 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/AGENTS.md +2975 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/README.md +123 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/SKILL.md +137 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md +55 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/rules/advanced-init-once.md +42 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/rules/advanced-use-latest.md +39 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/rules/async-api-routes.md +38 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/rules/async-defer-await.md +80 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/rules/async-dependencies.md +51 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/rules/async-parallel.md +28 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md +99 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md +59 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/rules/bundle-conditional.md +31 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md +49 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md +35 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/rules/bundle-preload.md +50 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/rules/client-event-listeners.md +74 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/rules/client-localstorage-schema.md +71 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md +48 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/rules/client-swr-dedup.md +56 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/rules/js-batch-dom-css.md +107 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/rules/js-cache-function-results.md +80 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/rules/js-cache-property-access.md +28 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/rules/js-cache-storage.md +70 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/rules/js-combine-iterations.md +32 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/rules/js-early-exit.md +50 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/rules/js-hoist-regexp.md +45 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/rules/js-index-maps.md +37 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/rules/js-length-check-first.md +49 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/rules/js-min-max-loop.md +82 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/rules/js-set-map-lookups.md +24 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md +57 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/rules/rendering-activity.md +26 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/rules/rendering-conditional-render.md +40 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/rules/rendering-content-visibility.md +38 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md +46 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md +30 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/rules/rendering-svg-precision.md +28 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md +75 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/rules/rerender-defer-reads.md +39 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/rules/rerender-dependencies.md +45 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md +40 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/rules/rerender-derived-state.md +29 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md +74 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md +58 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md +38 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/rules/rerender-memo.md +44 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md +45 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md +35 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/rules/rerender-transitions.md +40 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md +73 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/rules/server-after-nonblocking.md +73 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/rules/server-auth-actions.md +96 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/rules/server-cache-lru.md +41 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/rules/server-cache-react.md +76 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/rules/server-dedup-props.md +65 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/rules/server-hoist-static-io.md +142 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/rules/server-parallel-fetching.md +83 -0
- package/dist/vendor/harness-opencode/dist/skills/vercel-react-best-practices/rules/server-serialization.md +38 -0
- package/dist/vendor/harness-opencode/dist/skills/web-design-guidelines/SKILL.md +39 -0
- package/dist/vendor/harness-opencode/package.json +11 -0
- package/package.json +20 -15
- package/LICENSE +0 -21
- package/dist/chunk-TU23AE2F.js +0 -69
|
@@ -0,0 +1,2092 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AGENT_TIERS,
|
|
3
|
+
createAgents,
|
|
4
|
+
formatModelOverrideWarning,
|
|
5
|
+
validateModelOverride
|
|
6
|
+
} from "./chunk-XCZ3NOXR.js";
|
|
7
|
+
import {
|
|
8
|
+
PACKAGE_NAME,
|
|
9
|
+
readOurPackageVersion,
|
|
10
|
+
refreshPluginCache
|
|
11
|
+
} from "./chunk-VJUETC6A.js";
|
|
12
|
+
|
|
13
|
+
// src/config-hook.ts
|
|
14
|
+
import * as fs from "fs";
|
|
15
|
+
import * as path from "path";
|
|
16
|
+
import * as os from "os";
|
|
17
|
+
|
|
18
|
+
// src/commands/index.ts
|
|
19
|
+
import { readFileSync } from "fs";
|
|
20
|
+
import { fileURLToPath } from "url";
|
|
21
|
+
import { dirname, join } from "path";
|
|
22
|
+
var HERE = dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
function readPrompt(name) {
|
|
24
|
+
const candidates = [
|
|
25
|
+
join(HERE, "prompts", name),
|
|
26
|
+
// dev: src/commands/prompts/
|
|
27
|
+
join(HERE, "commands", "prompts", name),
|
|
28
|
+
// dist: dist/ → dist/commands/prompts/
|
|
29
|
+
join(HERE, "..", "..", "src", "commands", "prompts", name)
|
|
30
|
+
// fallback dev
|
|
31
|
+
];
|
|
32
|
+
for (const p of candidates) {
|
|
33
|
+
try {
|
|
34
|
+
return readFileSync(p, "utf8");
|
|
35
|
+
} catch {
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
throw new Error(`Could not find command prompt: ${name}`);
|
|
39
|
+
}
|
|
40
|
+
var autopilotPrompt = readPrompt("autopilot.md");
|
|
41
|
+
var shipPrompt = readPrompt("ship.md");
|
|
42
|
+
var reviewPrompt = readPrompt("review.md");
|
|
43
|
+
var initDeepPrompt = readPrompt("init-deep.md");
|
|
44
|
+
var researchPrompt = readPrompt("research.md");
|
|
45
|
+
var freshPrompt = readPrompt("fresh.md");
|
|
46
|
+
var costsPrompt = readPrompt("costs.md");
|
|
47
|
+
function createCommands() {
|
|
48
|
+
return {
|
|
49
|
+
autopilot: {
|
|
50
|
+
template: autopilotPrompt,
|
|
51
|
+
description: "Self-driving run. Pass a ticket ref (any tracker), a task description, or a question."
|
|
52
|
+
},
|
|
53
|
+
ship: {
|
|
54
|
+
template: shipPrompt,
|
|
55
|
+
description: "Finalize, commit, push, and open a PR/MR. Human-gated at each step."
|
|
56
|
+
},
|
|
57
|
+
review: {
|
|
58
|
+
template: reviewPrompt,
|
|
59
|
+
description: "Adversarial read-only review of a PR, current branch, commit range, or file."
|
|
60
|
+
},
|
|
61
|
+
"init-deep": {
|
|
62
|
+
template: initDeepPrompt,
|
|
63
|
+
description: "Generate hierarchical AGENTS.md files for the current repo."
|
|
64
|
+
},
|
|
65
|
+
research: {
|
|
66
|
+
template: researchPrompt,
|
|
67
|
+
description: "Deep codebase exploration via parallel subagents."
|
|
68
|
+
},
|
|
69
|
+
fresh: {
|
|
70
|
+
template: freshPrompt,
|
|
71
|
+
description: "Re-key the current worktree to a new task. Runs the repo's .glorious/hooks/fresh-reset if present; otherwise discards local changes and creates a new branch from latest origin/<default>. Then continues inline into the PRIME on the new task."
|
|
72
|
+
},
|
|
73
|
+
costs: {
|
|
74
|
+
template: costsPrompt,
|
|
75
|
+
description: "Show running LLM cost totals accrued by the cost-tracker plugin. Pass --json or --log for raw data."
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// src/mcp/index.ts
|
|
81
|
+
function createMcpConfig() {
|
|
82
|
+
const memoryLauncherCmd = [
|
|
83
|
+
"bash",
|
|
84
|
+
"-c",
|
|
85
|
+
// Use node's require.resolve to find the bundled launcher inside the
|
|
86
|
+
// installed package, then exec it. Works because the MCP command runs
|
|
87
|
+
// in a CJS-compatible shell context.
|
|
88
|
+
`exec bash "$(node -e 'process.stdout.write(require.resolve("@glrs-dev/harness-plugin-opencode/dist/bin/memory-mcp-launcher.sh"))')"`
|
|
89
|
+
];
|
|
90
|
+
return {
|
|
91
|
+
serena: {
|
|
92
|
+
type: "local",
|
|
93
|
+
command: [
|
|
94
|
+
"uvx",
|
|
95
|
+
"--from",
|
|
96
|
+
"git+https://github.com/oraios/serena",
|
|
97
|
+
"serena",
|
|
98
|
+
"start-mcp-server",
|
|
99
|
+
"--context=ide-assistant",
|
|
100
|
+
"--open-web-dashboard",
|
|
101
|
+
"false"
|
|
102
|
+
],
|
|
103
|
+
enabled: true
|
|
104
|
+
},
|
|
105
|
+
memory: {
|
|
106
|
+
type: "local",
|
|
107
|
+
command: memoryLauncherCmd,
|
|
108
|
+
enabled: true
|
|
109
|
+
},
|
|
110
|
+
git: {
|
|
111
|
+
type: "local",
|
|
112
|
+
command: ["uvx", "mcp-server-git"],
|
|
113
|
+
enabled: true
|
|
114
|
+
},
|
|
115
|
+
playwright: {
|
|
116
|
+
type: "local",
|
|
117
|
+
command: ["npx", "-y", "@playwright/mcp"],
|
|
118
|
+
enabled: false
|
|
119
|
+
},
|
|
120
|
+
linear: {
|
|
121
|
+
type: "remote",
|
|
122
|
+
url: "https://mcp.linear.app/mcp",
|
|
123
|
+
enabled: false
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// src/skills/paths.ts
|
|
129
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
130
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
131
|
+
function getSkillsRoot() {
|
|
132
|
+
const here = dirname2(fileURLToPath2(import.meta.url));
|
|
133
|
+
return join2(here, "skills");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// src/config-hook.ts
|
|
137
|
+
function writePermDebugSnapshot(config) {
|
|
138
|
+
if (process.env["HARNESS_OPENCODE_PERM_DEBUG"] !== "1") return;
|
|
139
|
+
try {
|
|
140
|
+
const stateDir = process.env["XDG_STATE_HOME"] || path.join(os.homedir(), ".local", "state");
|
|
141
|
+
const targetDir = path.join(stateDir, "harness-opencode");
|
|
142
|
+
const targetFile = path.join(targetDir, "perm-debug.json");
|
|
143
|
+
const version = readOurPackageVersion(import.meta.url);
|
|
144
|
+
const agentBlock = config.agent ?? {};
|
|
145
|
+
const agentPerms = {};
|
|
146
|
+
for (const [name, cfg] of Object.entries(agentBlock)) {
|
|
147
|
+
agentPerms[name] = cfg?.permission ?? null;
|
|
148
|
+
}
|
|
149
|
+
const payload = {
|
|
150
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
151
|
+
pluginVersion: version,
|
|
152
|
+
agents: Object.keys(agentBlock),
|
|
153
|
+
agentPermissions: agentPerms,
|
|
154
|
+
// Include the global permission block too — useful context when
|
|
155
|
+
// diagnosing interplay between global and per-agent rules.
|
|
156
|
+
globalPermission: config.permission ?? null
|
|
157
|
+
};
|
|
158
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
159
|
+
fs.writeFileSync(targetFile, JSON.stringify(payload, null, 2));
|
|
160
|
+
} catch {
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function resolveHarnessModels(agents, config, pluginOptions) {
|
|
164
|
+
const modelsConfig = pluginOptions?.models ?? config.harness?.models;
|
|
165
|
+
if (!modelsConfig) return agents;
|
|
166
|
+
const warnedIds = /* @__PURE__ */ new Set();
|
|
167
|
+
const warnIfInvalid = (value, source) => {
|
|
168
|
+
const result = validateModelOverride(value);
|
|
169
|
+
if (result.valid) return;
|
|
170
|
+
if (warnedIds.has(value)) return;
|
|
171
|
+
warnedIds.add(value);
|
|
172
|
+
console.warn(formatModelOverrideWarning(value, source, result.suggestion));
|
|
173
|
+
};
|
|
174
|
+
for (const [agentName, agentCfg] of Object.entries(agents)) {
|
|
175
|
+
const perAgent = modelsConfig[agentName];
|
|
176
|
+
if (perAgent !== void 0) {
|
|
177
|
+
const picked = Array.isArray(perAgent) ? perAgent[0] : perAgent;
|
|
178
|
+
agentCfg.model = picked;
|
|
179
|
+
warnIfInvalid(picked, `models.${agentName}`);
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
const tier = AGENT_TIERS[agentName];
|
|
183
|
+
if (tier) {
|
|
184
|
+
const perTier = modelsConfig[tier];
|
|
185
|
+
if (perTier !== void 0) {
|
|
186
|
+
const picked = Array.isArray(perTier) ? perTier[0] : perTier;
|
|
187
|
+
agentCfg.model = picked;
|
|
188
|
+
warnIfInvalid(picked, `models.${tier}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return agents;
|
|
193
|
+
}
|
|
194
|
+
function applyConfig(config, pluginOptions) {
|
|
195
|
+
const ourAgents = createAgents();
|
|
196
|
+
resolveHarnessModels(ourAgents, config, pluginOptions);
|
|
197
|
+
config.agent = { ...ourAgents, ...config.agent ?? {} };
|
|
198
|
+
const ourCommands = createCommands();
|
|
199
|
+
config.command = {
|
|
200
|
+
...ourCommands,
|
|
201
|
+
...config.command ?? {}
|
|
202
|
+
};
|
|
203
|
+
const ourMcp = createMcpConfig();
|
|
204
|
+
config.mcp = { ...ourMcp, ...config.mcp ?? {} };
|
|
205
|
+
const skillsRoot = getSkillsRoot();
|
|
206
|
+
const existingSkills = config.skills ?? {};
|
|
207
|
+
const existingPaths = Array.isArray(existingSkills.paths) ? existingSkills.paths : [];
|
|
208
|
+
const existingUrls = Array.isArray(existingSkills.urls) ? existingSkills.urls : [];
|
|
209
|
+
config.skills = {
|
|
210
|
+
...existingSkills,
|
|
211
|
+
paths: [skillsRoot, ...existingPaths],
|
|
212
|
+
urls: existingUrls
|
|
213
|
+
};
|
|
214
|
+
if (!config.default_agent) {
|
|
215
|
+
config.default_agent = "prime";
|
|
216
|
+
}
|
|
217
|
+
const existingPermission = config.permission ?? {};
|
|
218
|
+
const existingExtDir = existingPermission.external_directory ?? {};
|
|
219
|
+
config.permission = {
|
|
220
|
+
...existingPermission,
|
|
221
|
+
external_directory: {
|
|
222
|
+
"~/.glorious/worktrees/**": "allow",
|
|
223
|
+
"~/.glorious/opencode/**": "allow",
|
|
224
|
+
// repo-shared plan storage (see src/plan-paths.ts) + cost-tracker data
|
|
225
|
+
"/tmp/**": "allow",
|
|
226
|
+
"/private/tmp/**": "allow",
|
|
227
|
+
// macOS: /tmp symlinks to /private/tmp
|
|
228
|
+
"/var/folders/**/T/**": "allow",
|
|
229
|
+
// macOS $TMPDIR expansion
|
|
230
|
+
"~/.config/opencode/**": "allow",
|
|
231
|
+
// OpenCode's own config dir — agents read it routinely
|
|
232
|
+
"~/.config/crush/**": "allow",
|
|
233
|
+
// sibling AI tool config — agents read it routinely
|
|
234
|
+
"~/.cache/**": "allow",
|
|
235
|
+
// XDG cache dir — tooling (npm, pip, etc.) writes here
|
|
236
|
+
"~/.local/share/**": "allow",
|
|
237
|
+
// XDG data dir — Linear MCP cache, etc.
|
|
238
|
+
"~/.local/state/**": "allow",
|
|
239
|
+
// XDG state dir — includes plugin spill at harness-opencode/tool-output/ and perm-debug.json
|
|
240
|
+
...existingExtDir
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
writePermDebugSnapshot(config);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// src/tools/ast_grep.ts
|
|
247
|
+
import { tool } from "@opencode-ai/plugin";
|
|
248
|
+
import { execFile } from "child_process";
|
|
249
|
+
import { promisify } from "util";
|
|
250
|
+
var exec = promisify(execFile);
|
|
251
|
+
var ast_grep_default = tool({
|
|
252
|
+
description: "Search or rewrite TypeScript/JavaScript/Python/etc. by AST pattern using ast-grep. Use this instead of grep when you need structural matching: function signatures, JSX patterns, import shapes, decorator usage, etc. Patterns use $VAR for captures.",
|
|
253
|
+
args: {
|
|
254
|
+
pattern: tool.schema.string().describe(
|
|
255
|
+
"ast-grep pattern, e.g. 'console.log($MSG)' or 'function $NAME($$$ARGS) { $$$BODY }'"
|
|
256
|
+
),
|
|
257
|
+
rewrite: tool.schema.string().optional().describe(
|
|
258
|
+
"If set, rewrites matches to this template. Use $VAR to reference captures."
|
|
259
|
+
),
|
|
260
|
+
paths: tool.schema.array(tool.schema.string()).default(["."]).describe("Files or directories to search"),
|
|
261
|
+
language: tool.schema.enum(["ts", "tsx", "js", "jsx", "py", "go", "rs"]).optional().describe("Language hint; auto-detected by extension if omitted"),
|
|
262
|
+
dryRun: tool.schema.boolean().default(true).describe("If false and rewrite is set, applies changes to disk")
|
|
263
|
+
},
|
|
264
|
+
async execute(args, context) {
|
|
265
|
+
const cmdArgs = ["run", "--pattern", args.pattern];
|
|
266
|
+
if (args.rewrite) cmdArgs.push("--rewrite", args.rewrite);
|
|
267
|
+
if (args.language) cmdArgs.push("--lang", args.language);
|
|
268
|
+
if (args.rewrite && !args.dryRun) cmdArgs.push("--update-all");
|
|
269
|
+
cmdArgs.push(...args.paths);
|
|
270
|
+
try {
|
|
271
|
+
const { stdout, stderr } = await exec("ast-grep", cmdArgs, {
|
|
272
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
273
|
+
cwd: context.directory,
|
|
274
|
+
encoding: "utf8"
|
|
275
|
+
});
|
|
276
|
+
const out = String(stdout || "(no matches)");
|
|
277
|
+
const warn = stderr ? `
|
|
278
|
+
[warnings]
|
|
279
|
+
${String(stderr)}` : "";
|
|
280
|
+
return out + warn;
|
|
281
|
+
} catch (err) {
|
|
282
|
+
const e = err;
|
|
283
|
+
return `ast-grep error: ${e.message}${e.stderr ? "\n" + e.stderr : ""}`;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// src/tools/tsc_check.ts
|
|
289
|
+
import { tool as tool2 } from "@opencode-ai/plugin";
|
|
290
|
+
import { execFile as execFile2 } from "child_process";
|
|
291
|
+
import { promisify as promisify2 } from "util";
|
|
292
|
+
var exec2 = promisify2(execFile2);
|
|
293
|
+
var MAX_ROWS = 15;
|
|
294
|
+
var MAX_BUFFER = 2 * 1024 * 1024;
|
|
295
|
+
var REMEDIATION_HINTS = {
|
|
296
|
+
TS2322: "Assigned type isn't assignable \u2014 narrow with a guard or widen the target type",
|
|
297
|
+
TS2345: "Argument type doesn't match param \u2014 check param type & coerce or narrow",
|
|
298
|
+
TS2531: "Value is possibly null \u2014 guard with `if (x)` or use optional chaining",
|
|
299
|
+
TS18048: "Value is possibly undefined \u2014 guard before use or assert non-null",
|
|
300
|
+
TS2307: "Module not found \u2014 install the dep or fix the import path",
|
|
301
|
+
TS7006: "Implicit `any` on parameter \u2014 annotate the type explicitly",
|
|
302
|
+
TS2339: "Property doesn't exist on type \u2014 check the type, widen it, or use indexing",
|
|
303
|
+
TS2304: "Name not found \u2014 missing import or typo",
|
|
304
|
+
TS2532: "Object possibly undefined \u2014 guard with `?.` or narrow before access",
|
|
305
|
+
TS18047: "Value is possibly null \u2014 add a null guard",
|
|
306
|
+
TS2769: "No overload matches \u2014 check argument types/arity against signatures",
|
|
307
|
+
TS2741: "Missing required property \u2014 add it to the object literal",
|
|
308
|
+
TS2739: "Missing required properties \u2014 add all listed fields",
|
|
309
|
+
TS2554: "Wrong number of arguments \u2014 check the function signature",
|
|
310
|
+
TS2551: "Property not found; did you mean <suggestion>? \u2014 fix the name",
|
|
311
|
+
TS7016: "No type declarations \u2014 add `@types/<pkg>` or `declare module`",
|
|
312
|
+
TS2367: "Comparison always false \u2014 types don't overlap; rework the check",
|
|
313
|
+
TS1005: "Syntax error \u2014 check brackets/commas/semicolons near the report",
|
|
314
|
+
TS1109: "Expression expected \u2014 syntax malformation; re-check the line",
|
|
315
|
+
TS2420: "Class incorrectly implements interface \u2014 add/align missing members"
|
|
316
|
+
};
|
|
317
|
+
function parseTscOutput(raw) {
|
|
318
|
+
const re = /^(.+?)\((\d+),(\d+)\):\s+error\s+TS(\d+):\s+(.+)$/;
|
|
319
|
+
const out = [];
|
|
320
|
+
for (const line of raw.split("\n")) {
|
|
321
|
+
const m = re.exec(line);
|
|
322
|
+
if (!m) continue;
|
|
323
|
+
out.push({
|
|
324
|
+
file: m[1],
|
|
325
|
+
line: Number(m[2]),
|
|
326
|
+
col: Number(m[3]),
|
|
327
|
+
code: `TS${m[4]}`,
|
|
328
|
+
message: m[5]
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
return out;
|
|
332
|
+
}
|
|
333
|
+
function dedupeAndCap(errors, cap) {
|
|
334
|
+
const map = /* @__PURE__ */ new Map();
|
|
335
|
+
for (const e of errors) {
|
|
336
|
+
const key = `${e.code}::${e.file}`;
|
|
337
|
+
const existing = map.get(key);
|
|
338
|
+
if (existing) {
|
|
339
|
+
existing.count += 1;
|
|
340
|
+
} else {
|
|
341
|
+
map.set(key, { ...e, count: 1 });
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
const all = [...map.values()].sort((a, b) => {
|
|
345
|
+
if (b.count !== a.count) return b.count - a.count;
|
|
346
|
+
return a.code.localeCompare(b.code);
|
|
347
|
+
});
|
|
348
|
+
if (all.length <= cap) return { rows: all, truncated: 0 };
|
|
349
|
+
return { rows: all.slice(0, cap), truncated: all.length - cap };
|
|
350
|
+
}
|
|
351
|
+
function formatRow(row) {
|
|
352
|
+
const hint = REMEDIATION_HINTS[row.code];
|
|
353
|
+
const hintSuffix = hint ? `
|
|
354
|
+
\u2192 ${hint}` : "";
|
|
355
|
+
const countSuffix = row.count > 1 ? ` (\xD7${row.count})` : "";
|
|
356
|
+
return `${row.file}:${row.line}:${row.col} ${row.code}${countSuffix}: ${row.message}${hintSuffix}`;
|
|
357
|
+
}
|
|
358
|
+
var tsc_check_default = tool2({
|
|
359
|
+
description: "Run TypeScript compiler in noEmit mode on the project. Returns errors only. Faster than running the full test suite for type-correctness checks.",
|
|
360
|
+
args: {
|
|
361
|
+
project: tool2.schema.string().default("tsconfig.json").describe("Path to tsconfig.json (relative to the project directory)"),
|
|
362
|
+
full: tool2.schema.boolean().default(false).describe(
|
|
363
|
+
"If true, bypass the 15-row (code,file) dedupe/cap and return every error"
|
|
364
|
+
)
|
|
365
|
+
},
|
|
366
|
+
async execute(args, context) {
|
|
367
|
+
let raw;
|
|
368
|
+
try {
|
|
369
|
+
const { stdout, stderr } = await exec2(
|
|
370
|
+
"npx",
|
|
371
|
+
["tsc", "--noEmit", "--project", args.project, "--pretty", "false"],
|
|
372
|
+
{ maxBuffer: MAX_BUFFER, cwd: context.directory, encoding: "utf8" }
|
|
373
|
+
);
|
|
374
|
+
raw = String(stdout || "");
|
|
375
|
+
if (stderr) raw += `
|
|
376
|
+
[warnings]
|
|
377
|
+
${String(stderr)}`;
|
|
378
|
+
} catch (err) {
|
|
379
|
+
const e = err;
|
|
380
|
+
if (e.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER" || /\bENOBUFS\b/.test(e.message ?? "")) {
|
|
381
|
+
return `[tsc output overflowed ${MAX_BUFFER} byte buffer \u2014 too many errors to stream. Pass full:true is NOT recommended here; fix the top-level errors first and re-run.]`;
|
|
382
|
+
}
|
|
383
|
+
raw = String(e.stdout || e.message || "");
|
|
384
|
+
}
|
|
385
|
+
if (!raw.trim()) return "(no errors)";
|
|
386
|
+
const errors = parseTscOutput(raw);
|
|
387
|
+
if (errors.length === 0) {
|
|
388
|
+
return raw;
|
|
389
|
+
}
|
|
390
|
+
if (args.full) {
|
|
391
|
+
const lines2 = errors.map((e) => formatRow({ ...e, count: 1 }));
|
|
392
|
+
return `Total errors: ${errors.length}
|
|
393
|
+
|
|
394
|
+
${lines2.join("\n")}`;
|
|
395
|
+
}
|
|
396
|
+
const { rows, truncated } = dedupeAndCap(errors, MAX_ROWS);
|
|
397
|
+
const lines = rows.map(formatRow);
|
|
398
|
+
const footer = truncated > 0 ? `
|
|
399
|
+
|
|
400
|
+
\u2026 ${truncated} more (code,file) categories (pass full:true to see all). Total raw errors: ${errors.length}.` : `
|
|
401
|
+
|
|
402
|
+
Total raw errors: ${errors.length}.`;
|
|
403
|
+
return `${lines.join("\n")}${footer}`;
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
// src/tools/eslint_check.ts
|
|
408
|
+
import { tool as tool3 } from "@opencode-ai/plugin";
|
|
409
|
+
import { execFile as execFile3 } from "child_process";
|
|
410
|
+
import { promisify as promisify3 } from "util";
|
|
411
|
+
var exec3 = promisify3(execFile3);
|
|
412
|
+
var MAX_ROWS2 = 50;
|
|
413
|
+
var MAX_BUFFER2 = 2 * 1024 * 1024;
|
|
414
|
+
var ESLINT_REMEDIATION_HINTS = {
|
|
415
|
+
"no-unused-vars": "Remove the binding or prefix with `_` to mark intentional",
|
|
416
|
+
"no-explicit-any": "Replace `any` with a real type or `unknown` + narrowing",
|
|
417
|
+
"prefer-const": "Binding is never reassigned \u2014 use `const` instead of `let`",
|
|
418
|
+
"no-console": "Remove the console call or route through a real logger",
|
|
419
|
+
eqeqeq: "Use `===` / `!==` instead of `==` / `!=`",
|
|
420
|
+
"no-empty": "Empty block \u2014 add a comment or handle the case explicitly",
|
|
421
|
+
"no-shadow": "Rename to avoid shadowing the outer binding",
|
|
422
|
+
"no-undef": "Missing import or typo; add to imports or declare globally",
|
|
423
|
+
"no-var": "Replace `var` with `let` or `const`",
|
|
424
|
+
semi: "Add or remove the trailing semicolon per the config",
|
|
425
|
+
quotes: "Use the configured quote style (single vs double)",
|
|
426
|
+
indent: "Re-indent to match the config; run the formatter",
|
|
427
|
+
"no-restricted-syntax": "Usage blocked by config \u2014 pick an allowed alternative",
|
|
428
|
+
"@typescript-eslint/no-floating-promises": "Await, `.catch()`, or `void`-prefix the promise",
|
|
429
|
+
"@typescript-eslint/no-misused-promises": "Handler can't return a promise; wrap or refactor"
|
|
430
|
+
};
|
|
431
|
+
function parseEslintJson(raw) {
|
|
432
|
+
if (!raw.trim()) return [];
|
|
433
|
+
let data;
|
|
434
|
+
try {
|
|
435
|
+
data = JSON.parse(raw);
|
|
436
|
+
} catch {
|
|
437
|
+
return [];
|
|
438
|
+
}
|
|
439
|
+
const out = [];
|
|
440
|
+
for (const file of data) {
|
|
441
|
+
for (const msg of file.messages) {
|
|
442
|
+
out.push({
|
|
443
|
+
rule: msg.ruleId ?? "<parse-error>",
|
|
444
|
+
file: file.filePath,
|
|
445
|
+
line: msg.line,
|
|
446
|
+
message: msg.message,
|
|
447
|
+
severity: msg.severity,
|
|
448
|
+
count: 1
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return out;
|
|
453
|
+
}
|
|
454
|
+
function dedupeAndCap2(rows, cap) {
|
|
455
|
+
const map = /* @__PURE__ */ new Map();
|
|
456
|
+
for (const r of rows) {
|
|
457
|
+
const key = `${r.rule}::${r.file}`;
|
|
458
|
+
const existing = map.get(key);
|
|
459
|
+
if (existing) {
|
|
460
|
+
existing.count += 1;
|
|
461
|
+
} else {
|
|
462
|
+
map.set(key, { ...r });
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
const all = [...map.values()].sort((a, b) => {
|
|
466
|
+
if (b.severity !== a.severity) return b.severity - a.severity;
|
|
467
|
+
if (b.count !== a.count) return b.count - a.count;
|
|
468
|
+
return a.rule.localeCompare(b.rule);
|
|
469
|
+
});
|
|
470
|
+
if (all.length <= cap) return { rows: all, truncated: 0 };
|
|
471
|
+
return { rows: all.slice(0, cap), truncated: all.length - cap };
|
|
472
|
+
}
|
|
473
|
+
function formatRow2(r) {
|
|
474
|
+
const sev = r.severity === 2 ? "error" : "warn";
|
|
475
|
+
const hint = ESLINT_REMEDIATION_HINTS[r.rule];
|
|
476
|
+
const hintSuffix = hint ? `
|
|
477
|
+
\u2192 ${hint}` : "";
|
|
478
|
+
const countSuffix = r.count > 1 ? ` (\xD7${r.count})` : "";
|
|
479
|
+
return `${r.file}:${r.line} [${sev}] ${r.rule}${countSuffix}: ${r.message}${hintSuffix}`;
|
|
480
|
+
}
|
|
481
|
+
var eslint_check_default = tool3({
|
|
482
|
+
description: "Run eslint on specific files. Returns lint errors as JSON.",
|
|
483
|
+
args: {
|
|
484
|
+
files: tool3.schema.array(tool3.schema.string()).describe("Files or globs to lint"),
|
|
485
|
+
fix: tool3.schema.boolean().default(false).describe("If true, auto-fix safe issues"),
|
|
486
|
+
full: tool3.schema.boolean().default(false).describe(
|
|
487
|
+
"If true, bypass the 50-row (rule,file) dedupe/cap and return every violation"
|
|
488
|
+
)
|
|
489
|
+
},
|
|
490
|
+
async execute(args, context) {
|
|
491
|
+
const cmdArgs = ["eslint", "--format", "json"];
|
|
492
|
+
if (args.fix) cmdArgs.push("--fix");
|
|
493
|
+
cmdArgs.push(...args.files);
|
|
494
|
+
let raw;
|
|
495
|
+
try {
|
|
496
|
+
const { stdout } = await exec3("npx", cmdArgs, {
|
|
497
|
+
maxBuffer: MAX_BUFFER2,
|
|
498
|
+
cwd: context.directory,
|
|
499
|
+
encoding: "utf8"
|
|
500
|
+
});
|
|
501
|
+
raw = String(stdout || "[]");
|
|
502
|
+
} catch (err) {
|
|
503
|
+
const e = err;
|
|
504
|
+
if (e.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER" || /\bENOBUFS\b/.test(e.message ?? "")) {
|
|
505
|
+
return `[eslint output overflowed ${MAX_BUFFER2} byte buffer. Fix the top-level rules first and re-run on narrower globs.]`;
|
|
506
|
+
}
|
|
507
|
+
raw = String(e.stdout || `eslint error: ${e.message}`);
|
|
508
|
+
}
|
|
509
|
+
const rows = parseEslintJson(raw);
|
|
510
|
+
if (rows.length === 0) {
|
|
511
|
+
return raw;
|
|
512
|
+
}
|
|
513
|
+
if (args.full) {
|
|
514
|
+
const lines2 = rows.map((r) => formatRow2(r));
|
|
515
|
+
return `Total violations: ${rows.length}
|
|
516
|
+
|
|
517
|
+
${lines2.join("\n")}`;
|
|
518
|
+
}
|
|
519
|
+
const { rows: capped, truncated } = dedupeAndCap2(rows, MAX_ROWS2);
|
|
520
|
+
const lines = capped.map(formatRow2);
|
|
521
|
+
const footer = truncated > 0 ? `
|
|
522
|
+
|
|
523
|
+
\u2026 ${truncated} more (rule,file) categories (pass full:true to see all). Total raw violations: ${rows.length}.` : `
|
|
524
|
+
|
|
525
|
+
Total raw violations: ${rows.length}.`;
|
|
526
|
+
return `${lines.join("\n")}${footer}`;
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
// src/tools/todo_scan.ts
|
|
531
|
+
import { tool as tool4 } from "@opencode-ai/plugin";
|
|
532
|
+
import { execFile as execFile4 } from "child_process";
|
|
533
|
+
import { promisify as promisify4 } from "util";
|
|
534
|
+
var exec4 = promisify4(execFile4);
|
|
535
|
+
var todo_scan_default = tool4({
|
|
536
|
+
description: "Scan files for TODO / FIXME / HACK / XXX comments. Returns a structured list of matches (file:line:type:text). Use `onlyChanged: true` to restrict to files changed vs HEAD \u2014 useful for QA review to catch tech debt introduced in the current change. Returns plain-text output, one match per line.",
|
|
537
|
+
args: {
|
|
538
|
+
paths: tool4.schema.array(tool4.schema.string()).default(["."]).describe("Files or directories to scan (ignored if onlyChanged is true)"),
|
|
539
|
+
onlyChanged: tool4.schema.boolean().default(false).describe("If true, scan only files modified vs HEAD (git diff --name-only)"),
|
|
540
|
+
types: tool4.schema.array(tool4.schema.enum(["TODO", "FIXME", "HACK", "XXX"])).default(["TODO", "FIXME", "HACK", "XXX"]).describe("Annotation types to look for"),
|
|
541
|
+
maxResults: tool4.schema.number().default(200).describe("Cap on number of matches returned")
|
|
542
|
+
},
|
|
543
|
+
async execute(args, context) {
|
|
544
|
+
let scanPaths = args.paths;
|
|
545
|
+
if (args.onlyChanged) {
|
|
546
|
+
try {
|
|
547
|
+
const { stdout } = await exec4(
|
|
548
|
+
"git",
|
|
549
|
+
["diff", "--name-only", "HEAD"],
|
|
550
|
+
{ cwd: context.directory, encoding: "utf8", maxBuffer: 10 * 1024 * 1024 }
|
|
551
|
+
);
|
|
552
|
+
scanPaths = String(stdout).split("\n").filter(Boolean);
|
|
553
|
+
if (scanPaths.length === 0) return "(no changed files)";
|
|
554
|
+
} catch (err) {
|
|
555
|
+
const e = err;
|
|
556
|
+
return `git diff failed: ${e.message}`;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
const pattern = `(${args.types.join("|")})\\b`;
|
|
560
|
+
const rgArgs = [
|
|
561
|
+
"--line-number",
|
|
562
|
+
"--no-heading",
|
|
563
|
+
"--color=never",
|
|
564
|
+
"-e",
|
|
565
|
+
pattern,
|
|
566
|
+
...scanPaths
|
|
567
|
+
];
|
|
568
|
+
try {
|
|
569
|
+
const { stdout } = await exec4("rg", rgArgs, {
|
|
570
|
+
cwd: context.directory,
|
|
571
|
+
encoding: "utf8",
|
|
572
|
+
maxBuffer: 10 * 1024 * 1024
|
|
573
|
+
});
|
|
574
|
+
const lines = String(stdout).split("\n").filter(Boolean);
|
|
575
|
+
const capped = lines.slice(0, args.maxResults);
|
|
576
|
+
const suffix = lines.length > args.maxResults ? `
|
|
577
|
+
|
|
578
|
+
[truncated: ${lines.length - args.maxResults} additional matches \u2014 narrow paths or raise maxResults]` : "";
|
|
579
|
+
return capped.length > 0 ? capped.join("\n") + suffix : `(no ${args.types.join("/")} matches in scanned paths)`;
|
|
580
|
+
} catch (err) {
|
|
581
|
+
const e = err;
|
|
582
|
+
if (e.code === 1) return `(no ${args.types.join("/")} matches in scanned paths)`;
|
|
583
|
+
return `rg error: ${e.message}`;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
// src/tools/comment_check.ts
|
|
589
|
+
import { tool as tool5 } from "@opencode-ai/plugin";
|
|
590
|
+
import { execFile as execFile5 } from "child_process";
|
|
591
|
+
import { promisify as promisify5 } from "util";
|
|
592
|
+
var exec5 = promisify5(execFile5);
|
|
593
|
+
var DEFAULT_TYPES = ["TODO", "FIXME", "HACK", "XXX", "DEPRECATED"];
|
|
594
|
+
var comment_check_default = tool5({
|
|
595
|
+
description: "Find attributed code annotations like @TODO(alice), @FIXME, @HACK, @XXX, @DEPRECATED. Returns structured matches with author if captured, plus optional age-in-days via git blame. Use this before planning or editing an area to inventory known tech debt.",
|
|
596
|
+
args: {
|
|
597
|
+
paths: tool5.schema.array(tool5.schema.string()).default(["."]).describe("Files or directories to scan"),
|
|
598
|
+
types: tool5.schema.array(tool5.schema.string()).default([...DEFAULT_TYPES]).describe("Annotation types to surface (e.g. TODO, FIXME, HACK, DEPRECATED)"),
|
|
599
|
+
includeAge: tool5.schema.boolean().default(false).describe(
|
|
600
|
+
"If true, run git blame per match to determine age in days (slow on large result sets)"
|
|
601
|
+
),
|
|
602
|
+
maxResults: tool5.schema.number().default(30).describe("Cap on matches returned (default reduced from 100 to 30 to limit context flooding)")
|
|
603
|
+
},
|
|
604
|
+
async execute(args, context) {
|
|
605
|
+
const typesAlt = args.types.join("|");
|
|
606
|
+
const pattern = `@(${typesAlt})(\\(([^)]+)\\))?`;
|
|
607
|
+
const rgArgs = [
|
|
608
|
+
"--line-number",
|
|
609
|
+
"--no-heading",
|
|
610
|
+
"--color=never",
|
|
611
|
+
"-e",
|
|
612
|
+
pattern,
|
|
613
|
+
...args.paths
|
|
614
|
+
];
|
|
615
|
+
let raw;
|
|
616
|
+
try {
|
|
617
|
+
const { stdout } = await exec5("rg", rgArgs, {
|
|
618
|
+
cwd: context.directory,
|
|
619
|
+
encoding: "utf8",
|
|
620
|
+
maxBuffer: 10 * 1024 * 1024
|
|
621
|
+
});
|
|
622
|
+
raw = String(stdout);
|
|
623
|
+
} catch (err) {
|
|
624
|
+
const e = err;
|
|
625
|
+
if (e.code === 1) return "(no annotations found)";
|
|
626
|
+
return `rg error: ${e.message}`;
|
|
627
|
+
}
|
|
628
|
+
const annotRe = new RegExp(`@(${typesAlt})(?:\\(([^)]+)\\))?`);
|
|
629
|
+
const lineRe = /^(.+?):(\d+):(.*)$/;
|
|
630
|
+
const rows = [];
|
|
631
|
+
const lines = raw.split("\n").filter(Boolean);
|
|
632
|
+
for (const line of lines) {
|
|
633
|
+
const parts = line.match(lineRe);
|
|
634
|
+
if (!parts) continue;
|
|
635
|
+
const [, file, lineStr, text] = parts;
|
|
636
|
+
const am = text.match(annotRe);
|
|
637
|
+
if (!am) continue;
|
|
638
|
+
const type = am[1];
|
|
639
|
+
const author = am[2] ?? "";
|
|
640
|
+
let age = "";
|
|
641
|
+
let ageDays = -1;
|
|
642
|
+
if (args.includeAge) {
|
|
643
|
+
try {
|
|
644
|
+
const { stdout: blame } = await exec5(
|
|
645
|
+
"git",
|
|
646
|
+
["log", "-1", "--format=%ct", "-L", `${lineStr},${lineStr}:${file}`],
|
|
647
|
+
{ cwd: context.directory, encoding: "utf8", maxBuffer: 1024 * 1024 }
|
|
648
|
+
);
|
|
649
|
+
const ts = parseInt(String(blame).trim().split("\n")[0] ?? "", 10);
|
|
650
|
+
if (!Number.isNaN(ts)) {
|
|
651
|
+
ageDays = Math.floor((Date.now() / 1e3 - ts) / 86400);
|
|
652
|
+
age = ` (${ageDays}d old)`;
|
|
653
|
+
}
|
|
654
|
+
} catch {
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
const authorPart = author ? ` [${author}]` : "";
|
|
658
|
+
rows.push({
|
|
659
|
+
text: `${file}:${lineStr} @${type}${authorPart}${age} \u2014 ${text.trim().slice(0, 200)}`,
|
|
660
|
+
ageDays
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
if (rows.length === 0) return "(no annotations found)";
|
|
664
|
+
if (args.includeAge) {
|
|
665
|
+
rows.sort((a, b) => {
|
|
666
|
+
if (a.ageDays === -1 && b.ageDays === -1) return 0;
|
|
667
|
+
if (a.ageDays === -1) return 1;
|
|
668
|
+
if (b.ageDays === -1) return -1;
|
|
669
|
+
return b.ageDays - a.ageDays;
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
const capped = rows.slice(0, args.maxResults);
|
|
673
|
+
const truncated = rows.length > args.maxResults ? `
|
|
674
|
+
|
|
675
|
+
[truncated: ${rows.length - args.maxResults} more]` : "";
|
|
676
|
+
return capped.map((r) => r.text).join("\n") + truncated;
|
|
677
|
+
}
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
// src/tools/index.ts
|
|
681
|
+
function createTools() {
|
|
682
|
+
return {
|
|
683
|
+
ast_grep: ast_grep_default,
|
|
684
|
+
tsc_check: tsc_check_default,
|
|
685
|
+
eslint_check: eslint_check_default,
|
|
686
|
+
todo_scan: todo_scan_default,
|
|
687
|
+
comment_check: comment_check_default
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// src/plugins/dotenv.ts
|
|
692
|
+
import * as fs2 from "fs";
|
|
693
|
+
import * as path2 from "path";
|
|
694
|
+
var DOTENV_FILES = [".env", ".env.local"];
|
|
695
|
+
function parseDotenv(content) {
|
|
696
|
+
const out = {};
|
|
697
|
+
for (const raw of content.split(/\r?\n/)) {
|
|
698
|
+
let line = raw.trim();
|
|
699
|
+
if (!line || line.startsWith("#")) continue;
|
|
700
|
+
if (line.startsWith("export ")) {
|
|
701
|
+
line = line.slice(7).trimStart();
|
|
702
|
+
}
|
|
703
|
+
const eq = line.indexOf("=");
|
|
704
|
+
if (eq === -1) continue;
|
|
705
|
+
const key = line.slice(0, eq).trim();
|
|
706
|
+
if (!key) continue;
|
|
707
|
+
let val = line.slice(eq + 1);
|
|
708
|
+
const trimmedVal = val.trimStart();
|
|
709
|
+
if (trimmedVal.startsWith('"') && trimmedVal.endsWith('"') || trimmedVal.startsWith("'") && trimmedVal.endsWith("'")) {
|
|
710
|
+
val = trimmedVal.slice(1, -1);
|
|
711
|
+
} else {
|
|
712
|
+
const hashIdx = val.indexOf(" #");
|
|
713
|
+
if (hashIdx !== -1) {
|
|
714
|
+
val = val.slice(0, hashIdx);
|
|
715
|
+
}
|
|
716
|
+
val = val.trim();
|
|
717
|
+
}
|
|
718
|
+
out[key] = val;
|
|
719
|
+
}
|
|
720
|
+
return out;
|
|
721
|
+
}
|
|
722
|
+
function loadDotenv(directory) {
|
|
723
|
+
const merged = {};
|
|
724
|
+
const filesLoaded = [];
|
|
725
|
+
for (const name of DOTENV_FILES) {
|
|
726
|
+
const filePath = path2.join(directory, name);
|
|
727
|
+
let content;
|
|
728
|
+
try {
|
|
729
|
+
content = fs2.readFileSync(filePath, "utf8");
|
|
730
|
+
} catch {
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
733
|
+
const parsed = parseDotenv(content);
|
|
734
|
+
Object.assign(merged, parsed);
|
|
735
|
+
filesLoaded.push(name);
|
|
736
|
+
}
|
|
737
|
+
let varsSet = 0;
|
|
738
|
+
for (const [key, value] of Object.entries(merged)) {
|
|
739
|
+
if (!(key in process.env)) {
|
|
740
|
+
process.env[key] = value;
|
|
741
|
+
varsSet++;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
return { filesLoaded, varsSet };
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// src/plugins/autopilot.ts
|
|
748
|
+
import { execFile as execFileCb } from "child_process";
|
|
749
|
+
import * as fs3 from "fs/promises";
|
|
750
|
+
import * as path3 from "path";
|
|
751
|
+
import { promisify as promisify6 } from "util";
|
|
752
|
+
var STATE_PATH = ".agent/autopilot-state.json";
|
|
753
|
+
var KILL_SWITCH_PATH = ".agent/autopilot-disable";
|
|
754
|
+
var MAX_ITERATIONS = 20;
|
|
755
|
+
var TARGET_AGENTS = /* @__PURE__ */ new Set(["build", "prime"]);
|
|
756
|
+
var MESSAGE_LIMIT = 40;
|
|
757
|
+
var NUDGE_DEBOUNCE_MS = 3e4;
|
|
758
|
+
var PR_CACHE_MS = 5 * 60 * 1e3;
|
|
759
|
+
var MAX_CONSECUTIVE_STOPS = 2;
|
|
760
|
+
var UMBRELLA_MIN_BYTES = 5e4;
|
|
761
|
+
var UMBRELLA_MIN_LINEAR_IDS = 3;
|
|
762
|
+
var AUTOPILOT_MARKER_RE = /(^|\s)\/autopilot(\s|$)|AUTOPILOT mode/;
|
|
763
|
+
var OPT_OUT_RE = /<!--\s*autopilot:\s*(skip|false)\s*-->/i;
|
|
764
|
+
var UMBRELLA_SECTION_RE = /^##\s+(Chunks|Milestones|Workstreams)\b/m;
|
|
765
|
+
var LINEAR_ID_RE = /\b[A-Z]{2,10}-\d+\b/g;
|
|
766
|
+
var MEASUREMENT_GATE_RE = /\b(7-day|production window|post-deploy|post-launch|SLO|success rate reaches|after deploy|bake time)\b/i;
|
|
767
|
+
var STOP_REPORT_RE = /^STOP[:.\s—]/m;
|
|
768
|
+
var NUDGE_TEXT = "[autopilot] Session idled with unchecked acceptance criteria. Re-read the plan, do the most important unchecked item, check its box when done, then move to the next. When all boxes are `[x]`, print the Phase 5 handoff and stop \u2014 the user runs `/ship` manually.";
|
|
769
|
+
var MAX_ITERATIONS_TEXT = `[autopilot] Stopped: hit max iterations (${MAX_ITERATIONS}). Either the work is complete or the loop is stuck. Review and resume manually; a new \`/autopilot\` session will re-enable nudges.`;
|
|
770
|
+
async function readState(dir) {
|
|
771
|
+
try {
|
|
772
|
+
const raw = await fs3.readFile(path3.join(dir, STATE_PATH), "utf8");
|
|
773
|
+
const parsed = JSON.parse(raw);
|
|
774
|
+
return { sessions: parsed.sessions ?? {} };
|
|
775
|
+
} catch {
|
|
776
|
+
return { sessions: {} };
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
async function writeState(dir, state) {
|
|
780
|
+
const p = path3.join(dir, STATE_PATH);
|
|
781
|
+
await fs3.mkdir(path3.dirname(p), { recursive: true });
|
|
782
|
+
await fs3.writeFile(p, JSON.stringify(state, null, 2) + "\n", "utf8");
|
|
783
|
+
}
|
|
784
|
+
function userText(msg) {
|
|
785
|
+
if (msg.info?.role !== "user") return "";
|
|
786
|
+
const parts = msg.parts ?? [];
|
|
787
|
+
return parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join("\n");
|
|
788
|
+
}
|
|
789
|
+
function latestUserAgent(messages) {
|
|
790
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
791
|
+
const info = messages[i].info;
|
|
792
|
+
if (info?.role === "user" && typeof info.agent === "string") {
|
|
793
|
+
return info.agent;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
return void 0;
|
|
797
|
+
}
|
|
798
|
+
function latestAssistantText(messages) {
|
|
799
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
800
|
+
const msg = messages[i];
|
|
801
|
+
if (msg.info?.role !== "assistant") continue;
|
|
802
|
+
const parts = msg.parts ?? [];
|
|
803
|
+
return parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join("\n");
|
|
804
|
+
}
|
|
805
|
+
return "";
|
|
806
|
+
}
|
|
807
|
+
function detectActivation(messages) {
|
|
808
|
+
for (const msg of messages) {
|
|
809
|
+
if (msg.info?.role !== "user") continue;
|
|
810
|
+
return AUTOPILOT_MARKER_RE.test(userText(msg));
|
|
811
|
+
}
|
|
812
|
+
return false;
|
|
813
|
+
}
|
|
814
|
+
var PLAN_PATH_RE = /(?:\.agent\/plans\/[\w-]+\.md|(?:\/[^\s`"']*)?\/[\w.-]+\/plans\/[\w-]+\.md)/;
|
|
815
|
+
function findPlanPath(messages) {
|
|
816
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
817
|
+
const parts = messages[i].parts ?? [];
|
|
818
|
+
for (const part of parts) {
|
|
819
|
+
if (part.type === "text" && typeof part.text === "string") {
|
|
820
|
+
const m = part.text.match(PLAN_PATH_RE);
|
|
821
|
+
if (m) return m[0];
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
return null;
|
|
826
|
+
}
|
|
827
|
+
function countUnchecked(planContent) {
|
|
828
|
+
const section = /## Acceptance criteria([\s\S]*?)(?=\n##|$)/.exec(planContent);
|
|
829
|
+
if (!section) return 0;
|
|
830
|
+
const matches = section[1].match(/^- \[ \]/gm);
|
|
831
|
+
return matches?.length ?? 0;
|
|
832
|
+
}
|
|
833
|
+
function classifyPlan(content) {
|
|
834
|
+
if (OPT_OUT_RE.test(content)) return "opted-out";
|
|
835
|
+
if (UMBRELLA_SECTION_RE.test(content)) return "umbrella";
|
|
836
|
+
if (content.length > UMBRELLA_MIN_BYTES) return "umbrella";
|
|
837
|
+
const linearIds = content.match(LINEAR_ID_RE) ?? [];
|
|
838
|
+
const unique = new Set(linearIds);
|
|
839
|
+
if (unique.size >= UMBRELLA_MIN_LINEAR_IDS) return "umbrella";
|
|
840
|
+
const acSection = /## Acceptance criteria([\s\S]*?)(?=\n##|$)/.exec(content);
|
|
841
|
+
if (acSection && MEASUREMENT_GATE_RE.test(acSection[1])) {
|
|
842
|
+
return "measurement-gated";
|
|
843
|
+
}
|
|
844
|
+
return "unit";
|
|
845
|
+
}
|
|
846
|
+
function planGoalLinearId(content) {
|
|
847
|
+
const goal = /## Goal([\s\S]*?)(?=\n##|$)/.exec(content);
|
|
848
|
+
if (!goal) return null;
|
|
849
|
+
const m = goal[1].match(LINEAR_ID_RE);
|
|
850
|
+
return m ? m[0] : null;
|
|
851
|
+
}
|
|
852
|
+
function detectStopReport(assistantText) {
|
|
853
|
+
if (!assistantText) return false;
|
|
854
|
+
return STOP_REPORT_RE.test(assistantText);
|
|
855
|
+
}
|
|
856
|
+
var execFile6 = promisify6(execFileCb);
|
|
857
|
+
async function currentBranch(dir) {
|
|
858
|
+
try {
|
|
859
|
+
const { stdout } = await execFile6(
|
|
860
|
+
"git",
|
|
861
|
+
["-C", dir, "branch", "--show-current"],
|
|
862
|
+
{ timeout: 2e3 }
|
|
863
|
+
);
|
|
864
|
+
const branch = stdout.trim();
|
|
865
|
+
return branch.length > 0 ? branch : null;
|
|
866
|
+
} catch {
|
|
867
|
+
return null;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
async function pullRequestState(dir) {
|
|
871
|
+
try {
|
|
872
|
+
const { stdout } = await execFile6(
|
|
873
|
+
"gh",
|
|
874
|
+
["pr", "view", "--json", "state", "--jq", ".state"],
|
|
875
|
+
{ cwd: dir, timeout: 5e3 }
|
|
876
|
+
);
|
|
877
|
+
const state = stdout.trim();
|
|
878
|
+
return state.length > 0 ? state : null;
|
|
879
|
+
} catch {
|
|
880
|
+
return null;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
async function killSwitchEngaged(dir) {
|
|
884
|
+
try {
|
|
885
|
+
await fs3.access(path3.join(dir, KILL_SWITCH_PATH));
|
|
886
|
+
return true;
|
|
887
|
+
} catch {
|
|
888
|
+
return false;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
async function sendNudge(client, sessionID, sessState, text, now = Date.now()) {
|
|
892
|
+
if (sessState.lastNudgeAt !== void 0 && now - sessState.lastNudgeAt < NUDGE_DEBOUNCE_MS) {
|
|
893
|
+
return false;
|
|
894
|
+
}
|
|
895
|
+
await client.session.promptAsync({
|
|
896
|
+
path: { id: sessionID },
|
|
897
|
+
body: { parts: [{ type: "text", text }] }
|
|
898
|
+
});
|
|
899
|
+
sessState.lastNudgeAt = now;
|
|
900
|
+
return true;
|
|
901
|
+
}
|
|
902
|
+
var plugin = async ({ client, directory }) => {
|
|
903
|
+
return {
|
|
904
|
+
event: async ({ event }) => {
|
|
905
|
+
if (event.type !== "session.idle") return;
|
|
906
|
+
const sessionID = event.properties.sessionID;
|
|
907
|
+
const msgsResp = await client.session.messages({
|
|
908
|
+
path: { id: sessionID },
|
|
909
|
+
query: { limit: MESSAGE_LIMIT }
|
|
910
|
+
});
|
|
911
|
+
const messages = msgsResp.data ?? [];
|
|
912
|
+
const agent = latestUserAgent(messages);
|
|
913
|
+
if (!agent || !TARGET_AGENTS.has(agent)) return;
|
|
914
|
+
const state = await readState(directory);
|
|
915
|
+
const sessState = state.sessions[sessionID] ?? {
|
|
916
|
+
iterations: 0
|
|
917
|
+
};
|
|
918
|
+
if (sessState.stopped) return;
|
|
919
|
+
if (!sessState.enabled) {
|
|
920
|
+
if (!detectActivation(messages)) return;
|
|
921
|
+
sessState.enabled = true;
|
|
922
|
+
}
|
|
923
|
+
if (await killSwitchEngaged(directory)) {
|
|
924
|
+
state.sessions[sessionID] = {
|
|
925
|
+
...sessState,
|
|
926
|
+
stopped: true,
|
|
927
|
+
stopReason: "kill-switch"
|
|
928
|
+
};
|
|
929
|
+
await writeState(directory, state);
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
if (sessState.iterations >= MAX_ITERATIONS) {
|
|
933
|
+
await sendNudge(client, sessionID, sessState, MAX_ITERATIONS_TEXT);
|
|
934
|
+
state.sessions[sessionID] = {
|
|
935
|
+
...sessState,
|
|
936
|
+
stopped: true,
|
|
937
|
+
stopReason: "max-iterations"
|
|
938
|
+
};
|
|
939
|
+
await writeState(directory, state);
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
const planPath = findPlanPath(messages);
|
|
943
|
+
if (!planPath) return;
|
|
944
|
+
const resolvedPlanPath = path3.isAbsolute(planPath) ? planPath : path3.join(directory, planPath);
|
|
945
|
+
let planContent;
|
|
946
|
+
try {
|
|
947
|
+
planContent = await fs3.readFile(resolvedPlanPath, "utf8");
|
|
948
|
+
} catch {
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
const shape = classifyPlan(planContent);
|
|
952
|
+
if (shape !== "unit") {
|
|
953
|
+
state.sessions[sessionID] = {
|
|
954
|
+
...sessState,
|
|
955
|
+
stopped: true,
|
|
956
|
+
stopReason: `plan-shape:${shape}`
|
|
957
|
+
};
|
|
958
|
+
await writeState(directory, state);
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
const planLinearId = planGoalLinearId(planContent);
|
|
962
|
+
if (planLinearId) {
|
|
963
|
+
const branch = await currentBranch(directory);
|
|
964
|
+
if (branch && !branch.toLowerCase().includes(planLinearId.toLowerCase())) {
|
|
965
|
+
state.sessions[sessionID] = {
|
|
966
|
+
...sessState,
|
|
967
|
+
stopped: true,
|
|
968
|
+
stopReason: "branch-mismatch"
|
|
969
|
+
};
|
|
970
|
+
await writeState(directory, state);
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
const now = Date.now();
|
|
975
|
+
let prState = sessState.prState;
|
|
976
|
+
const prExpired = sessState.prCheckedAt === void 0 || now - sessState.prCheckedAt > PR_CACHE_MS;
|
|
977
|
+
if (prExpired) {
|
|
978
|
+
const fetched = await pullRequestState(directory);
|
|
979
|
+
prState = fetched ?? "none";
|
|
980
|
+
sessState.prState = prState;
|
|
981
|
+
sessState.prCheckedAt = now;
|
|
982
|
+
}
|
|
983
|
+
if (prState === "MERGED") {
|
|
984
|
+
state.sessions[sessionID] = {
|
|
985
|
+
...sessState,
|
|
986
|
+
stopped: true,
|
|
987
|
+
stopReason: "pr-merged"
|
|
988
|
+
};
|
|
989
|
+
await writeState(directory, state);
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
const unchecked = countUnchecked(planContent);
|
|
993
|
+
if (unchecked === 0) {
|
|
994
|
+
state.sessions[sessionID] = {
|
|
995
|
+
...sessState,
|
|
996
|
+
consecutiveStops: 0,
|
|
997
|
+
lastUncheckedCount: 0
|
|
998
|
+
};
|
|
999
|
+
await writeState(directory, state);
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
const lastUnchecked = sessState.lastUncheckedCount;
|
|
1003
|
+
const madeProgress = lastUnchecked !== void 0 && unchecked < lastUnchecked;
|
|
1004
|
+
const stopReported = detectStopReport(latestAssistantText(messages));
|
|
1005
|
+
let consecutiveStops = sessState.consecutiveStops ?? 0;
|
|
1006
|
+
if (madeProgress) {
|
|
1007
|
+
consecutiveStops = 0;
|
|
1008
|
+
} else if (stopReported) {
|
|
1009
|
+
consecutiveStops += 1;
|
|
1010
|
+
} else {
|
|
1011
|
+
}
|
|
1012
|
+
sessState.consecutiveStops = consecutiveStops;
|
|
1013
|
+
sessState.lastUncheckedCount = unchecked;
|
|
1014
|
+
if (consecutiveStops >= MAX_CONSECUTIVE_STOPS) {
|
|
1015
|
+
state.sessions[sessionID] = {
|
|
1016
|
+
...sessState,
|
|
1017
|
+
stopped: true,
|
|
1018
|
+
stopReason: "agent-stop-report"
|
|
1019
|
+
};
|
|
1020
|
+
await writeState(directory, state);
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
const sent = await sendNudge(client, sessionID, sessState, NUDGE_TEXT);
|
|
1024
|
+
if (sent) {
|
|
1025
|
+
state.sessions[sessionID] = {
|
|
1026
|
+
...sessState,
|
|
1027
|
+
iterations: sessState.iterations + 1
|
|
1028
|
+
};
|
|
1029
|
+
await writeState(directory, state);
|
|
1030
|
+
} else {
|
|
1031
|
+
state.sessions[sessionID] = { ...sessState };
|
|
1032
|
+
await writeState(directory, state);
|
|
1033
|
+
}
|
|
1034
|
+
},
|
|
1035
|
+
"chat.message": async ({ sessionID, agent }) => {
|
|
1036
|
+
if (!agent || !TARGET_AGENTS.has(agent)) return;
|
|
1037
|
+
const state = await readState(directory);
|
|
1038
|
+
const existing = state.sessions[sessionID];
|
|
1039
|
+
if (!existing?.enabled) return;
|
|
1040
|
+
state.sessions[sessionID] = {
|
|
1041
|
+
...existing,
|
|
1042
|
+
iterations: 0
|
|
1043
|
+
};
|
|
1044
|
+
await writeState(directory, state);
|
|
1045
|
+
}
|
|
1046
|
+
};
|
|
1047
|
+
};
|
|
1048
|
+
var autopilot_default = plugin;
|
|
1049
|
+
|
|
1050
|
+
// src/plugins/notify.ts
|
|
1051
|
+
var plugin2 = async ({ $, client }) => {
|
|
1052
|
+
async function notify(title, message) {
|
|
1053
|
+
if (process.platform === "darwin") {
|
|
1054
|
+
const esc = (s) => s.replace(/"/g, '\\"');
|
|
1055
|
+
await $`osascript -e ${`display notification "${esc(message)}" with title "${esc(title)}" sound name "Glass"`}`.nothrow();
|
|
1056
|
+
} else if (process.platform === "linux") {
|
|
1057
|
+
await $`notify-send ${title} ${message}`.nothrow();
|
|
1058
|
+
}
|
|
1059
|
+
try {
|
|
1060
|
+
await client.tui.showToast({
|
|
1061
|
+
body: { title, message, variant: "info", duration: 8e3 }
|
|
1062
|
+
});
|
|
1063
|
+
} catch {
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
return {
|
|
1067
|
+
// Notify when a permission prompt fires (replaces the old permission.asked event)
|
|
1068
|
+
"permission.ask": async (input, output) => {
|
|
1069
|
+
const tool6 = input?.tool ?? input?.title ?? "a tool";
|
|
1070
|
+
await notify("opencode permission required", `Approval needed for ${tool6}.`);
|
|
1071
|
+
}
|
|
1072
|
+
};
|
|
1073
|
+
};
|
|
1074
|
+
var notify_default = plugin2;
|
|
1075
|
+
|
|
1076
|
+
// src/plugins/cost-tracker.ts
|
|
1077
|
+
import * as fs4 from "fs/promises";
|
|
1078
|
+
import * as path4 from "path";
|
|
1079
|
+
import * as os2 from "os";
|
|
1080
|
+
var MAX_LINE_BYTES = 2048;
|
|
1081
|
+
var ROLLUP_DEBOUNCE_MS = 5e3;
|
|
1082
|
+
function resolveDataDir() {
|
|
1083
|
+
const override = process.env.GLORIOUS_COST_TRACKER_DIR;
|
|
1084
|
+
if (override) {
|
|
1085
|
+
if (override.startsWith("~")) {
|
|
1086
|
+
return path4.join(os2.homedir(), override.slice(1));
|
|
1087
|
+
}
|
|
1088
|
+
return override;
|
|
1089
|
+
}
|
|
1090
|
+
return path4.join(os2.homedir(), ".glorious", "opencode");
|
|
1091
|
+
}
|
|
1092
|
+
function zeroTokens() {
|
|
1093
|
+
return {
|
|
1094
|
+
input: 0,
|
|
1095
|
+
output: 0,
|
|
1096
|
+
reasoning: 0,
|
|
1097
|
+
cache: { read: 0, write: 0 }
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
function readTokens(src) {
|
|
1101
|
+
const s = src ?? {};
|
|
1102
|
+
const cache = s.cache ?? {};
|
|
1103
|
+
return {
|
|
1104
|
+
input: Number(s.input) || 0,
|
|
1105
|
+
output: Number(s.output) || 0,
|
|
1106
|
+
reasoning: Number(s.reasoning) || 0,
|
|
1107
|
+
cache: {
|
|
1108
|
+
read: Number(cache.read) || 0,
|
|
1109
|
+
write: Number(cache.write) || 0
|
|
1110
|
+
}
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
function subTokens(a, b) {
|
|
1114
|
+
return {
|
|
1115
|
+
input: a.input - b.input,
|
|
1116
|
+
output: a.output - b.output,
|
|
1117
|
+
reasoning: a.reasoning - b.reasoning,
|
|
1118
|
+
cache: {
|
|
1119
|
+
read: a.cache.read - b.cache.read,
|
|
1120
|
+
write: a.cache.write - b.cache.write
|
|
1121
|
+
}
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
function addTokens(a, b) {
|
|
1125
|
+
return {
|
|
1126
|
+
input: a.input + b.input,
|
|
1127
|
+
output: a.output + b.output,
|
|
1128
|
+
reasoning: a.reasoning + b.reasoning,
|
|
1129
|
+
cache: {
|
|
1130
|
+
read: a.cache.read + b.cache.read,
|
|
1131
|
+
write: a.cache.write + b.cache.write
|
|
1132
|
+
}
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
function clampTokens(t) {
|
|
1136
|
+
return {
|
|
1137
|
+
input: Math.max(0, t.input),
|
|
1138
|
+
output: Math.max(0, t.output),
|
|
1139
|
+
reasoning: Math.max(0, t.reasoning),
|
|
1140
|
+
cache: {
|
|
1141
|
+
read: Math.max(0, t.cache.read),
|
|
1142
|
+
write: Math.max(0, t.cache.write)
|
|
1143
|
+
}
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
function anyNonZero(cost, tokens) {
|
|
1147
|
+
return cost !== 0 || tokens.input !== 0 || tokens.output !== 0 || tokens.reasoning !== 0 || tokens.cache.read !== 0 || tokens.cache.write !== 0;
|
|
1148
|
+
}
|
|
1149
|
+
function emptyRollup() {
|
|
1150
|
+
return {
|
|
1151
|
+
version: 1,
|
|
1152
|
+
updatedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
1153
|
+
grandTotal: { cost: 0, tokens: zeroTokens(), messages: 0 },
|
|
1154
|
+
byProvider: {}
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
var plugin3 = async () => {
|
|
1158
|
+
if (process.env.GLORIOUS_COST_TRACKER === "0") {
|
|
1159
|
+
return {};
|
|
1160
|
+
}
|
|
1161
|
+
const dataDir = resolveDataDir();
|
|
1162
|
+
const jsonlPath = path4.join(dataDir, "costs.jsonl");
|
|
1163
|
+
const rollupPath = path4.join(dataDir, "costs.json");
|
|
1164
|
+
const lastSeen = /* @__PURE__ */ new Map();
|
|
1165
|
+
const messageMeta = /* @__PURE__ */ new Map();
|
|
1166
|
+
const rollup = emptyRollup();
|
|
1167
|
+
const warned = /* @__PURE__ */ new Set();
|
|
1168
|
+
function warnOnce(category, err) {
|
|
1169
|
+
if (warned.has(category)) return;
|
|
1170
|
+
warned.add(category);
|
|
1171
|
+
const msg = err instanceof Error ? err.message : typeof err === "string" ? err : String(err);
|
|
1172
|
+
process.stderr.write(`[cost-tracker] ${category}: ${msg}
|
|
1173
|
+
`);
|
|
1174
|
+
}
|
|
1175
|
+
let disabled = false;
|
|
1176
|
+
async function ensureDir() {
|
|
1177
|
+
if (disabled) return false;
|
|
1178
|
+
try {
|
|
1179
|
+
await fs4.mkdir(dataDir, { recursive: true });
|
|
1180
|
+
return true;
|
|
1181
|
+
} catch (err) {
|
|
1182
|
+
warnOnce("mkdir", err);
|
|
1183
|
+
disabled = true;
|
|
1184
|
+
return false;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
function applyToRollup(providerID, modelID, costDelta, tokensDelta, isFinalization, messageID) {
|
|
1188
|
+
const prov = rollup.byProvider[providerID] ??= {
|
|
1189
|
+
cost: 0,
|
|
1190
|
+
tokens: zeroTokens(),
|
|
1191
|
+
messages: 0,
|
|
1192
|
+
byModel: {}
|
|
1193
|
+
};
|
|
1194
|
+
const model = prov.byModel[modelID] ??= {
|
|
1195
|
+
cost: 0,
|
|
1196
|
+
tokens: zeroTokens(),
|
|
1197
|
+
messages: 0
|
|
1198
|
+
};
|
|
1199
|
+
prov.cost += costDelta;
|
|
1200
|
+
model.cost += costDelta;
|
|
1201
|
+
rollup.grandTotal.cost += costDelta;
|
|
1202
|
+
prov.tokens = addTokens(prov.tokens, tokensDelta);
|
|
1203
|
+
model.tokens = addTokens(model.tokens, tokensDelta);
|
|
1204
|
+
rollup.grandTotal.tokens = addTokens(rollup.grandTotal.tokens, tokensDelta);
|
|
1205
|
+
if (isFinalization) {
|
|
1206
|
+
const meta = messageMeta.get(messageID);
|
|
1207
|
+
if (meta && !meta.counted) {
|
|
1208
|
+
meta.counted = true;
|
|
1209
|
+
prov.messages += 1;
|
|
1210
|
+
model.messages += 1;
|
|
1211
|
+
rollup.grandTotal.messages += 1;
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
rollup.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1215
|
+
}
|
|
1216
|
+
let lastRollupWriteAt = 0;
|
|
1217
|
+
let rollupWriteInFlight = false;
|
|
1218
|
+
async function writeRollup(force) {
|
|
1219
|
+
if (disabled) return;
|
|
1220
|
+
const now = Date.now();
|
|
1221
|
+
if (!force && now - lastRollupWriteAt < ROLLUP_DEBOUNCE_MS) return;
|
|
1222
|
+
if (rollupWriteInFlight) return;
|
|
1223
|
+
rollupWriteInFlight = true;
|
|
1224
|
+
try {
|
|
1225
|
+
if (!await ensureDir()) return;
|
|
1226
|
+
const tmp = `${rollupPath}.tmp.${process.pid}.${Math.random().toString(36).slice(2, 10)}`;
|
|
1227
|
+
try {
|
|
1228
|
+
await fs4.writeFile(tmp, JSON.stringify(rollup, null, 2) + "\n", "utf8");
|
|
1229
|
+
await fs4.rename(tmp, rollupPath);
|
|
1230
|
+
lastRollupWriteAt = now;
|
|
1231
|
+
} catch (err) {
|
|
1232
|
+
warnOnce("rollup-write", err);
|
|
1233
|
+
try {
|
|
1234
|
+
await fs4.unlink(tmp);
|
|
1235
|
+
} catch {
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
} finally {
|
|
1239
|
+
rollupWriteInFlight = false;
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
async function appendJsonl(line) {
|
|
1243
|
+
if (disabled) return;
|
|
1244
|
+
if (!await ensureDir()) return;
|
|
1245
|
+
const text = JSON.stringify(line) + "\n";
|
|
1246
|
+
if (Buffer.byteLength(text, "utf8") > MAX_LINE_BYTES) {
|
|
1247
|
+
warnOnce(
|
|
1248
|
+
"line-too-long",
|
|
1249
|
+
`skipping event for ${line.messageID} \u2014 serialized size exceeds ${MAX_LINE_BYTES}B`
|
|
1250
|
+
);
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
try {
|
|
1254
|
+
await fs4.appendFile(jsonlPath, text, "utf8");
|
|
1255
|
+
} catch (err) {
|
|
1256
|
+
warnOnce("jsonl-append", err);
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
async function warmUp() {
|
|
1260
|
+
try {
|
|
1261
|
+
const raw = await fs4.readFile(jsonlPath, "utf8");
|
|
1262
|
+
const byMsg = /* @__PURE__ */ new Map();
|
|
1263
|
+
for (const rawLine of raw.split("\n")) {
|
|
1264
|
+
if (!rawLine) continue;
|
|
1265
|
+
let parsed;
|
|
1266
|
+
try {
|
|
1267
|
+
parsed = JSON.parse(rawLine);
|
|
1268
|
+
} catch {
|
|
1269
|
+
continue;
|
|
1270
|
+
}
|
|
1271
|
+
if (!parsed.messageID) continue;
|
|
1272
|
+
const existing = byMsg.get(parsed.messageID);
|
|
1273
|
+
const tokensTotal = readTokens(parsed.tokensTotal);
|
|
1274
|
+
const next = {
|
|
1275
|
+
providerID: parsed.providerID,
|
|
1276
|
+
modelID: parsed.modelID,
|
|
1277
|
+
costTotal: Number(parsed.costTotal) || 0,
|
|
1278
|
+
tokensTotal,
|
|
1279
|
+
everFinalized: (existing?.everFinalized ?? false) || !!parsed.finalized,
|
|
1280
|
+
lastCostTotal: Number(parsed.costTotal) || 0,
|
|
1281
|
+
lastTokensTotal: tokensTotal
|
|
1282
|
+
};
|
|
1283
|
+
byMsg.set(parsed.messageID, next);
|
|
1284
|
+
}
|
|
1285
|
+
for (const [messageID, state] of byMsg) {
|
|
1286
|
+
const prov = rollup.byProvider[state.providerID] ??= {
|
|
1287
|
+
cost: 0,
|
|
1288
|
+
tokens: zeroTokens(),
|
|
1289
|
+
messages: 0,
|
|
1290
|
+
byModel: {}
|
|
1291
|
+
};
|
|
1292
|
+
const model = prov.byModel[state.modelID] ??= {
|
|
1293
|
+
cost: 0,
|
|
1294
|
+
tokens: zeroTokens(),
|
|
1295
|
+
messages: 0
|
|
1296
|
+
};
|
|
1297
|
+
prov.cost += state.lastCostTotal;
|
|
1298
|
+
model.cost += state.lastCostTotal;
|
|
1299
|
+
rollup.grandTotal.cost += state.lastCostTotal;
|
|
1300
|
+
prov.tokens = addTokens(prov.tokens, state.lastTokensTotal);
|
|
1301
|
+
model.tokens = addTokens(model.tokens, state.lastTokensTotal);
|
|
1302
|
+
rollup.grandTotal.tokens = addTokens(
|
|
1303
|
+
rollup.grandTotal.tokens,
|
|
1304
|
+
state.lastTokensTotal
|
|
1305
|
+
);
|
|
1306
|
+
if (state.everFinalized) {
|
|
1307
|
+
prov.messages += 1;
|
|
1308
|
+
model.messages += 1;
|
|
1309
|
+
rollup.grandTotal.messages += 1;
|
|
1310
|
+
} else {
|
|
1311
|
+
lastSeen.set(messageID, {
|
|
1312
|
+
cost: state.lastCostTotal,
|
|
1313
|
+
tokens: state.lastTokensTotal
|
|
1314
|
+
});
|
|
1315
|
+
messageMeta.set(messageID, {
|
|
1316
|
+
providerID: state.providerID,
|
|
1317
|
+
modelID: state.modelID,
|
|
1318
|
+
// We've already added its costs to the rollup. On finalization
|
|
1319
|
+
// we'll apply only any remaining delta, and `counted: false`
|
|
1320
|
+
// lets us bump the message counter exactly once.
|
|
1321
|
+
counted: false
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
rollup.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1326
|
+
} catch (err) {
|
|
1327
|
+
const e = err;
|
|
1328
|
+
if (e && e.code === "ENOENT") {
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
warnOnce("warmup", err);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
await warmUp();
|
|
1335
|
+
return {
|
|
1336
|
+
event: async ({ event }) => {
|
|
1337
|
+
if (event.type !== "message.updated") return;
|
|
1338
|
+
const info = event.properties.info;
|
|
1339
|
+
if (!info || info.role !== "assistant") return;
|
|
1340
|
+
const assistantInfo = info;
|
|
1341
|
+
const messageID = String(assistantInfo.id ?? "");
|
|
1342
|
+
const sessionID = String(assistantInfo.sessionID ?? "");
|
|
1343
|
+
const providerID = String(assistantInfo.providerID ?? "unknown");
|
|
1344
|
+
const modelID = String(assistantInfo.modelID ?? "unknown");
|
|
1345
|
+
if (!messageID) return;
|
|
1346
|
+
const costNow = Number(assistantInfo.cost) || 0;
|
|
1347
|
+
const tokensNow = readTokens(assistantInfo.tokens);
|
|
1348
|
+
const finalized = assistantInfo.time != null && assistantInfo.time.completed != null;
|
|
1349
|
+
const existingMeta = messageMeta.get(messageID);
|
|
1350
|
+
messageMeta.set(messageID, {
|
|
1351
|
+
providerID,
|
|
1352
|
+
modelID,
|
|
1353
|
+
counted: existingMeta?.counted ?? false
|
|
1354
|
+
});
|
|
1355
|
+
const prior = lastSeen.get(messageID) ?? { cost: 0, tokens: zeroTokens() };
|
|
1356
|
+
let costDelta = costNow - prior.cost;
|
|
1357
|
+
let tokensDelta = subTokens(tokensNow, prior.tokens);
|
|
1358
|
+
if (costDelta < 0 || tokensDelta.input < 0 || tokensDelta.output < 0 || tokensDelta.reasoning < 0 || tokensDelta.cache.read < 0 || tokensDelta.cache.write < 0) {
|
|
1359
|
+
warnOnce(
|
|
1360
|
+
"negative-delta",
|
|
1361
|
+
`clamping negative delta on message ${messageID} (likely a retry/reset)`
|
|
1362
|
+
);
|
|
1363
|
+
costDelta = Math.max(0, costDelta);
|
|
1364
|
+
tokensDelta = clampTokens(tokensDelta);
|
|
1365
|
+
}
|
|
1366
|
+
lastSeen.set(messageID, { cost: costNow, tokens: tokensNow });
|
|
1367
|
+
const hasDelta = anyNonZero(costDelta, tokensDelta);
|
|
1368
|
+
if (!hasDelta && !finalized) {
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
if (hasDelta || finalized) {
|
|
1372
|
+
applyToRollup(providerID, modelID, costDelta, tokensDelta, finalized, messageID);
|
|
1373
|
+
}
|
|
1374
|
+
const line = {
|
|
1375
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1376
|
+
sessionID,
|
|
1377
|
+
messageID,
|
|
1378
|
+
providerID,
|
|
1379
|
+
modelID,
|
|
1380
|
+
costDelta,
|
|
1381
|
+
tokensDelta,
|
|
1382
|
+
costTotal: costNow,
|
|
1383
|
+
tokensTotal: tokensNow,
|
|
1384
|
+
finalized
|
|
1385
|
+
};
|
|
1386
|
+
await appendJsonl(line);
|
|
1387
|
+
await writeRollup(finalized);
|
|
1388
|
+
if (finalized) {
|
|
1389
|
+
lastSeen.delete(messageID);
|
|
1390
|
+
messageMeta.delete(messageID);
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
};
|
|
1394
|
+
};
|
|
1395
|
+
var cost_tracker_default = plugin3;
|
|
1396
|
+
|
|
1397
|
+
// src/plugins/pilot-plugin.ts
|
|
1398
|
+
import * as path5 from "path";
|
|
1399
|
+
var PILOT_SESSION_TITLE_PREFIX = "pilot/";
|
|
1400
|
+
var FORBIDDEN_BUILDER_BASH_PREFIXES = [
|
|
1401
|
+
"git commit",
|
|
1402
|
+
"git push",
|
|
1403
|
+
"git tag",
|
|
1404
|
+
"git checkout ",
|
|
1405
|
+
"git switch ",
|
|
1406
|
+
"git branch",
|
|
1407
|
+
"git restore --source",
|
|
1408
|
+
"git reset",
|
|
1409
|
+
"gh pr ",
|
|
1410
|
+
"gh release "
|
|
1411
|
+
];
|
|
1412
|
+
var EDIT_TOOLS = /* @__PURE__ */ new Set(["edit", "write", "patch", "multiedit"]);
|
|
1413
|
+
var plugin4 = async ({ client }) => {
|
|
1414
|
+
const sessionCache = /* @__PURE__ */ new Map();
|
|
1415
|
+
return {
|
|
1416
|
+
"tool.execute.before": async (input, output) => {
|
|
1417
|
+
const info = await classifySession(client, sessionCache, input.sessionID);
|
|
1418
|
+
if (info.kind === "pilot-builder") {
|
|
1419
|
+
if (input.tool === "bash") {
|
|
1420
|
+
enforceBuilderBashDeny(output.args);
|
|
1421
|
+
}
|
|
1422
|
+
} else if (info.kind === "pilot-planner") {
|
|
1423
|
+
if (EDIT_TOOLS.has(input.tool)) {
|
|
1424
|
+
await enforcePlannerEditScope(output.args, info.plansDir);
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
};
|
|
1429
|
+
};
|
|
1430
|
+
var pilot_plugin_default = plugin4;
|
|
1431
|
+
async function classifySession(client, cache, sessionID) {
|
|
1432
|
+
const cached = cache.get(sessionID);
|
|
1433
|
+
if (cached !== void 0) return cached;
|
|
1434
|
+
let title = "";
|
|
1435
|
+
let directory = "";
|
|
1436
|
+
try {
|
|
1437
|
+
const r = await client.session.get({ path: { id: sessionID } });
|
|
1438
|
+
const data = r.data;
|
|
1439
|
+
title = data?.title ?? "";
|
|
1440
|
+
directory = data?.directory ?? "";
|
|
1441
|
+
} catch {
|
|
1442
|
+
const v2 = { kind: "non-pilot" };
|
|
1443
|
+
cache.set(sessionID, v2);
|
|
1444
|
+
return v2;
|
|
1445
|
+
}
|
|
1446
|
+
if (title.startsWith(PILOT_SESSION_TITLE_PREFIX)) {
|
|
1447
|
+
const rest = title.slice(PILOT_SESSION_TITLE_PREFIX.length);
|
|
1448
|
+
const slash = rest.indexOf("/");
|
|
1449
|
+
if (slash > 0) {
|
|
1450
|
+
const runId = rest.slice(0, slash);
|
|
1451
|
+
const taskId = rest.slice(slash + 1);
|
|
1452
|
+
const v2 = { kind: "pilot-builder", runId, taskId };
|
|
1453
|
+
cache.set(sessionID, v2);
|
|
1454
|
+
return v2;
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
const plansDir = inferPlannerPlansDir(directory);
|
|
1458
|
+
if (plansDir !== null) {
|
|
1459
|
+
const v2 = { kind: "pilot-planner", plansDir };
|
|
1460
|
+
cache.set(sessionID, v2);
|
|
1461
|
+
return v2;
|
|
1462
|
+
}
|
|
1463
|
+
const v = { kind: "non-pilot" };
|
|
1464
|
+
cache.set(sessionID, v);
|
|
1465
|
+
return v;
|
|
1466
|
+
}
|
|
1467
|
+
function inferPlannerPlansDir(directory) {
|
|
1468
|
+
if (directory.length === 0) return null;
|
|
1469
|
+
const norm = directory.replace(/[\\/]+$/, "");
|
|
1470
|
+
const sepRegex = /[\\/]/;
|
|
1471
|
+
const parts = norm.split(sepRegex);
|
|
1472
|
+
if (parts.length < 2) return null;
|
|
1473
|
+
const last = parts[parts.length - 1];
|
|
1474
|
+
const prev = parts[parts.length - 2];
|
|
1475
|
+
if (last === "plans" && prev === "pilot") return norm;
|
|
1476
|
+
return null;
|
|
1477
|
+
}
|
|
1478
|
+
function enforceBuilderBashDeny(args) {
|
|
1479
|
+
const command = extractBashCommand(args);
|
|
1480
|
+
if (command === null) return;
|
|
1481
|
+
const trimmed = command.trimStart();
|
|
1482
|
+
for (const prefix of FORBIDDEN_BUILDER_BASH_PREFIXES) {
|
|
1483
|
+
if (trimmed.startsWith(prefix) || // Exact match for `git branch` (no trailing space) — the prefix
|
|
1484
|
+
// already has no trailing space; covered.
|
|
1485
|
+
trimmed === prefix.trimEnd()) {
|
|
1486
|
+
throw new Error(
|
|
1487
|
+
`pilot-plugin: pilot-builder is not permitted to run \`${prefix.trim()}\` commands (the worker handles commits/pushes/branches). If this is the right thing to do, respond with STOP: <reason>.`
|
|
1488
|
+
);
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
function extractBashCommand(args) {
|
|
1493
|
+
if (typeof args !== "object" || args === null) return null;
|
|
1494
|
+
const o = args;
|
|
1495
|
+
if (typeof o.command === "string") return o.command;
|
|
1496
|
+
if (typeof o.cmd === "string") return o.cmd;
|
|
1497
|
+
if (typeof o.body === "object" && o.body !== null && typeof o.body.command === "string") {
|
|
1498
|
+
return o.body.command;
|
|
1499
|
+
}
|
|
1500
|
+
return null;
|
|
1501
|
+
}
|
|
1502
|
+
async function enforcePlannerEditScope(args, plansDir) {
|
|
1503
|
+
const target = extractTargetPath(args);
|
|
1504
|
+
if (target === null) return;
|
|
1505
|
+
const abs = path5.isAbsolute(target) ? target : path5.resolve(plansDir, target);
|
|
1506
|
+
const normTarget = path5.normalize(abs);
|
|
1507
|
+
const normPlans = path5.normalize(plansDir).replace(/[\\/]+$/, "");
|
|
1508
|
+
if (normTarget === normPlans || normTarget.startsWith(normPlans + path5.sep) || normTarget.startsWith(normPlans + "/")) {
|
|
1509
|
+
return;
|
|
1510
|
+
}
|
|
1511
|
+
throw new Error(
|
|
1512
|
+
`pilot-plugin: pilot-planner is restricted to the plans directory (${plansDir}). The path ${JSON.stringify(target)} is outside scope. Save your YAML plan inside the plans dir.`
|
|
1513
|
+
);
|
|
1514
|
+
}
|
|
1515
|
+
function extractTargetPath(args) {
|
|
1516
|
+
if (typeof args !== "object" || args === null) return null;
|
|
1517
|
+
const o = args;
|
|
1518
|
+
if (typeof o.filePath === "string") return o.filePath;
|
|
1519
|
+
if (typeof o.path === "string") return o.path;
|
|
1520
|
+
if (typeof o.file === "string") return o.file;
|
|
1521
|
+
return null;
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
// src/plugins/tool-hooks.ts
|
|
1525
|
+
import * as crypto from "crypto";
|
|
1526
|
+
import * as fs5 from "fs";
|
|
1527
|
+
import * as path6 from "path";
|
|
1528
|
+
import * as os3 from "os";
|
|
1529
|
+
import { execFile as execFileCb2 } from "child_process";
|
|
1530
|
+
import { promisify as promisify7 } from "util";
|
|
1531
|
+
var exec6 = promisify7(execFileCb2);
|
|
1532
|
+
var EDIT_TOOLS2 = /* @__PURE__ */ new Set(["edit", "write", "patch", "multiedit"]);
|
|
1533
|
+
var TS_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx"]);
|
|
1534
|
+
var DEFAULT_BACKPRESSURE_THRESHOLD = 6e3;
|
|
1535
|
+
var DEFAULT_BACKPRESSURE_HEAD = 300;
|
|
1536
|
+
var DEFAULT_BACKPRESSURE_TAIL = 200;
|
|
1537
|
+
var DEFAULT_BACKPRESSURE_TOOLS = /* @__PURE__ */ new Set(["bash", "read", "glob", "grep"]);
|
|
1538
|
+
var DEFAULT_PER_TOOL_SHAPES = {
|
|
1539
|
+
read: "skip",
|
|
1540
|
+
// Read has its own limit/offset; double-truncation violates that contract.
|
|
1541
|
+
glob: "skip",
|
|
1542
|
+
// glob output is a path list; middle-truncation makes it unusable.
|
|
1543
|
+
bash: "tail",
|
|
1544
|
+
// Failures and exit codes are at the end of the stream.
|
|
1545
|
+
grep: "head-with-count"
|
|
1546
|
+
// First N matches verbatim + count tail; middle-truncation breaks match blocks.
|
|
1547
|
+
};
|
|
1548
|
+
var DEFAULT_GREP_HEAD_MATCHES = 20;
|
|
1549
|
+
var DEFAULT_BASH_TAIL_CHARS = 4e3;
|
|
1550
|
+
var DEFAULT_VERIFY_TIMEOUT_MS = 15e3;
|
|
1551
|
+
var TSC_MAX_BUFFER = 2 * 1024 * 1024;
|
|
1552
|
+
var VERIFY_MAX_ERRORS = 10;
|
|
1553
|
+
var DEFAULT_LOOP_THRESHOLD = 5;
|
|
1554
|
+
var sessions = /* @__PURE__ */ new Map();
|
|
1555
|
+
function getSession(sessionID) {
|
|
1556
|
+
let s = sessions.get(sessionID);
|
|
1557
|
+
if (!s) {
|
|
1558
|
+
s = {
|
|
1559
|
+
editCounts: /* @__PURE__ */ new Map(),
|
|
1560
|
+
readCache: /* @__PURE__ */ new Map(),
|
|
1561
|
+
callSeq: 0,
|
|
1562
|
+
lastVerifyTs: 0,
|
|
1563
|
+
directory: null
|
|
1564
|
+
};
|
|
1565
|
+
sessions.set(sessionID, s);
|
|
1566
|
+
}
|
|
1567
|
+
s.callSeq++;
|
|
1568
|
+
return s;
|
|
1569
|
+
}
|
|
1570
|
+
function isValidShape(s) {
|
|
1571
|
+
return s === "skip" || s === "head-tail" || s === "tail" || s === "head-with-count";
|
|
1572
|
+
}
|
|
1573
|
+
function resolveConfig(config, pluginOptions) {
|
|
1574
|
+
const raw = pluginOptions?.toolHooks ?? config.harness?.toolHooks ?? {};
|
|
1575
|
+
const bp = raw.backpressure ?? {};
|
|
1576
|
+
const vl = raw.verifyLoop ?? {};
|
|
1577
|
+
const ld = raw.loopDetection ?? {};
|
|
1578
|
+
const rd = raw.readDedup ?? {};
|
|
1579
|
+
const userPerTool = bp.perTool && typeof bp.perTool === "object" ? bp.perTool : {};
|
|
1580
|
+
const perTool = {};
|
|
1581
|
+
for (const tool6 of ["bash", "read", "glob", "grep"]) {
|
|
1582
|
+
const u = userPerTool[tool6] ?? {};
|
|
1583
|
+
perTool[tool6] = {
|
|
1584
|
+
threshold: typeof u.threshold === "number" ? u.threshold : void 0,
|
|
1585
|
+
headChars: typeof u.headChars === "number" ? u.headChars : void 0,
|
|
1586
|
+
tailChars: typeof u.tailChars === "number" ? u.tailChars : void 0,
|
|
1587
|
+
shape: isValidShape(u.shape) ? u.shape : DEFAULT_PER_TOOL_SHAPES[tool6] ?? "head-tail",
|
|
1588
|
+
grepHeadMatches: typeof u.grepHeadMatches === "number" ? u.grepHeadMatches : void 0
|
|
1589
|
+
};
|
|
1590
|
+
}
|
|
1591
|
+
return {
|
|
1592
|
+
backpressure: {
|
|
1593
|
+
enabled: bp.enabled !== false,
|
|
1594
|
+
threshold: typeof bp.threshold === "number" ? bp.threshold : DEFAULT_BACKPRESSURE_THRESHOLD,
|
|
1595
|
+
headChars: typeof bp.headChars === "number" ? bp.headChars : DEFAULT_BACKPRESSURE_HEAD,
|
|
1596
|
+
tailChars: typeof bp.tailChars === "number" ? bp.tailChars : DEFAULT_BACKPRESSURE_TAIL,
|
|
1597
|
+
tools: Array.isArray(bp.tools) ? new Set(bp.tools) : DEFAULT_BACKPRESSURE_TOOLS,
|
|
1598
|
+
perTool
|
|
1599
|
+
},
|
|
1600
|
+
verifyLoop: {
|
|
1601
|
+
enabled: vl.enabled !== false,
|
|
1602
|
+
timeoutMs: typeof vl.timeoutMs === "number" ? vl.timeoutMs : DEFAULT_VERIFY_TIMEOUT_MS
|
|
1603
|
+
},
|
|
1604
|
+
loopDetection: {
|
|
1605
|
+
enabled: ld.enabled !== false,
|
|
1606
|
+
threshold: typeof ld.threshold === "number" ? ld.threshold : DEFAULT_LOOP_THRESHOLD
|
|
1607
|
+
},
|
|
1608
|
+
readDedup: {
|
|
1609
|
+
enabled: rd.enabled !== false
|
|
1610
|
+
}
|
|
1611
|
+
};
|
|
1612
|
+
}
|
|
1613
|
+
function getToolOutputDir() {
|
|
1614
|
+
const stateHome = process.env["XDG_STATE_HOME"] || path6.join(os3.homedir(), ".local", "state");
|
|
1615
|
+
return path6.join(stateHome, "harness-opencode", "tool-output");
|
|
1616
|
+
}
|
|
1617
|
+
function hashContent(content) {
|
|
1618
|
+
return crypto.createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
1619
|
+
}
|
|
1620
|
+
function extractFilePath(args) {
|
|
1621
|
+
if (typeof args !== "object" || args === null) return null;
|
|
1622
|
+
const o = args;
|
|
1623
|
+
if (typeof o.filePath === "string") return o.filePath;
|
|
1624
|
+
if (typeof o.path === "string") return o.path;
|
|
1625
|
+
if (typeof o.file === "string") return o.file;
|
|
1626
|
+
return null;
|
|
1627
|
+
}
|
|
1628
|
+
function looksLikeBashFailure(output) {
|
|
1629
|
+
if (/Exit code:\s*[1-9]\d*/i.test(output)) return true;
|
|
1630
|
+
if (/\bexited with code [1-9]/i.test(output)) return true;
|
|
1631
|
+
if (/\bcommand failed\b/i.test(output)) return true;
|
|
1632
|
+
if (/\bERROR\b/.test(output) && output.length < 500) return true;
|
|
1633
|
+
return false;
|
|
1634
|
+
}
|
|
1635
|
+
async function resolveSessionDir(client, sess, sessionID) {
|
|
1636
|
+
if (sess.directory) return sess.directory;
|
|
1637
|
+
try {
|
|
1638
|
+
const r = await client.session.get({ path: { id: sessionID } });
|
|
1639
|
+
const data = r.data;
|
|
1640
|
+
sess.directory = data?.directory ?? process.cwd();
|
|
1641
|
+
} catch {
|
|
1642
|
+
sess.directory = process.cwd();
|
|
1643
|
+
}
|
|
1644
|
+
return sess.directory;
|
|
1645
|
+
}
|
|
1646
|
+
function isUnderToolOutputDir(filePath) {
|
|
1647
|
+
try {
|
|
1648
|
+
const abs = path6.resolve(filePath);
|
|
1649
|
+
const spillDir = path6.resolve(getToolOutputDir());
|
|
1650
|
+
return abs === spillDir || abs.startsWith(spillDir + path6.sep);
|
|
1651
|
+
} catch {
|
|
1652
|
+
return false;
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
function takeGrepHead(text, maxMatches) {
|
|
1656
|
+
const blocks = text.split(/\n\n+/);
|
|
1657
|
+
if (blocks.length <= maxMatches) {
|
|
1658
|
+
return { head: text, matchesKept: blocks.length, matchesOmitted: 0 };
|
|
1659
|
+
}
|
|
1660
|
+
const kept = blocks.slice(0, maxMatches);
|
|
1661
|
+
return {
|
|
1662
|
+
head: kept.join("\n\n"),
|
|
1663
|
+
matchesKept: kept.length,
|
|
1664
|
+
matchesOmitted: blocks.length - maxMatches
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
function applyBackpressure(cfg, toolName, callID, output, args) {
|
|
1668
|
+
if (!cfg.enabled) return;
|
|
1669
|
+
if (!cfg.tools.has(toolName)) return;
|
|
1670
|
+
const perTool = cfg.perTool[toolName];
|
|
1671
|
+
const shape = perTool?.shape ?? "head-tail";
|
|
1672
|
+
if (shape === "skip") return;
|
|
1673
|
+
if (toolName === "read") {
|
|
1674
|
+
const fp = extractFilePath(args);
|
|
1675
|
+
if (fp && isUnderToolOutputDir(fp)) return;
|
|
1676
|
+
}
|
|
1677
|
+
const text = output.output;
|
|
1678
|
+
const threshold = perTool?.threshold ?? cfg.threshold;
|
|
1679
|
+
if (text.length <= threshold) return;
|
|
1680
|
+
if (toolName === "bash" && looksLikeBashFailure(text)) return;
|
|
1681
|
+
let diskPath = null;
|
|
1682
|
+
try {
|
|
1683
|
+
const dir = getToolOutputDir();
|
|
1684
|
+
fs5.mkdirSync(dir, { recursive: true });
|
|
1685
|
+
diskPath = path6.join(dir, `${callID}.txt`);
|
|
1686
|
+
fs5.writeFileSync(diskPath, text);
|
|
1687
|
+
} catch {
|
|
1688
|
+
}
|
|
1689
|
+
const pathNote = diskPath ? ` Full output saved to: ${diskPath}` : "";
|
|
1690
|
+
if (shape === "tail") {
|
|
1691
|
+
const tailChars2 = perTool?.tailChars ?? (toolName === "bash" ? DEFAULT_BASH_TAIL_CHARS : cfg.tailChars);
|
|
1692
|
+
const tail2 = text.slice(-tailChars2);
|
|
1693
|
+
const omitted2 = text.length - tail2.length;
|
|
1694
|
+
output.output = `... [${omitted2} chars truncated \u2014 ${text.length} total.${pathNote}]
|
|
1695
|
+
|
|
1696
|
+
${tail2}`;
|
|
1697
|
+
return;
|
|
1698
|
+
}
|
|
1699
|
+
if (shape === "head-with-count") {
|
|
1700
|
+
const maxMatches = perTool?.grepHeadMatches ?? DEFAULT_GREP_HEAD_MATCHES;
|
|
1701
|
+
const { head: head2, matchesOmitted } = takeGrepHead(text, maxMatches);
|
|
1702
|
+
if (matchesOmitted === 0) {
|
|
1703
|
+
const fallbackHead = text.slice(0, perTool?.headChars ?? cfg.headChars);
|
|
1704
|
+
const fallbackTail = text.slice(-(perTool?.tailChars ?? cfg.tailChars));
|
|
1705
|
+
const omitted2 = text.length - fallbackHead.length - fallbackTail.length;
|
|
1706
|
+
output.output = `${fallbackHead}
|
|
1707
|
+
|
|
1708
|
+
... [${omitted2} chars truncated \u2014 ${text.length} total.${pathNote}]
|
|
1709
|
+
|
|
1710
|
+
${fallbackTail}`;
|
|
1711
|
+
return;
|
|
1712
|
+
}
|
|
1713
|
+
const spillNote = diskPath ? ` \u2014 full output at ${diskPath}` : "";
|
|
1714
|
+
output.output = `${head2}
|
|
1715
|
+
|
|
1716
|
+
... [${matchesOmitted} more matches${spillNote}]`;
|
|
1717
|
+
return;
|
|
1718
|
+
}
|
|
1719
|
+
const headChars = perTool?.headChars ?? cfg.headChars;
|
|
1720
|
+
const tailChars = perTool?.tailChars ?? cfg.tailChars;
|
|
1721
|
+
const head = text.slice(0, headChars);
|
|
1722
|
+
const tail = text.slice(-tailChars);
|
|
1723
|
+
const omitted = text.length - headChars - tailChars;
|
|
1724
|
+
output.output = `${head}
|
|
1725
|
+
|
|
1726
|
+
... [${omitted} chars truncated \u2014 ${text.length} total.${pathNote}]
|
|
1727
|
+
|
|
1728
|
+
${tail}`;
|
|
1729
|
+
}
|
|
1730
|
+
async function runPostEditVerify(cfg, client, sess, sessionID, filePath, output) {
|
|
1731
|
+
if (!cfg.enabled) return;
|
|
1732
|
+
const ext = path6.extname(filePath).toLowerCase();
|
|
1733
|
+
if (!TS_EXTENSIONS.has(ext)) return;
|
|
1734
|
+
const now = Date.now();
|
|
1735
|
+
if (now - sess.lastVerifyTs < 2e3) return;
|
|
1736
|
+
sess.lastVerifyTs = now;
|
|
1737
|
+
const cwd = await resolveSessionDir(client, sess, sessionID);
|
|
1738
|
+
try {
|
|
1739
|
+
const controller = new AbortController();
|
|
1740
|
+
const timer = setTimeout(() => controller.abort(), cfg.timeoutMs);
|
|
1741
|
+
let raw;
|
|
1742
|
+
try {
|
|
1743
|
+
const { stdout, stderr } = await exec6(
|
|
1744
|
+
"npx",
|
|
1745
|
+
["tsc", "--noEmit", "--pretty", "false"],
|
|
1746
|
+
{
|
|
1747
|
+
maxBuffer: TSC_MAX_BUFFER,
|
|
1748
|
+
cwd,
|
|
1749
|
+
encoding: "utf8",
|
|
1750
|
+
signal: controller.signal
|
|
1751
|
+
}
|
|
1752
|
+
);
|
|
1753
|
+
raw = String(stdout || "");
|
|
1754
|
+
if (stderr) raw += `
|
|
1755
|
+
${String(stderr)}`;
|
|
1756
|
+
} catch (err) {
|
|
1757
|
+
const e = err;
|
|
1758
|
+
if (e.killed || e.code === "ABORT_ERR") return;
|
|
1759
|
+
raw = String(e.stdout || "");
|
|
1760
|
+
} finally {
|
|
1761
|
+
clearTimeout(timer);
|
|
1762
|
+
}
|
|
1763
|
+
if (!raw.trim()) return;
|
|
1764
|
+
const errors = parseTscOutput(raw);
|
|
1765
|
+
const normPath = path6.resolve(cwd, filePath);
|
|
1766
|
+
const fileErrors = errors.filter((e) => {
|
|
1767
|
+
const errPath = path6.isAbsolute(e.file) ? e.file : path6.resolve(cwd, e.file);
|
|
1768
|
+
return path6.normalize(errPath) === path6.normalize(normPath);
|
|
1769
|
+
});
|
|
1770
|
+
if (fileErrors.length === 0) return;
|
|
1771
|
+
const { rows } = dedupeAndCap(fileErrors, VERIFY_MAX_ERRORS);
|
|
1772
|
+
const lines = rows.map(formatRow);
|
|
1773
|
+
output.output += `
|
|
1774
|
+
|
|
1775
|
+
--- POST-EDIT DIAGNOSTICS (${fileErrors.length} error${fileErrors.length !== 1 ? "s" : ""} in ${path6.basename(filePath)}) ---
|
|
1776
|
+
` + lines.join("\n") + `
|
|
1777
|
+
--- Fix these before proceeding ---`;
|
|
1778
|
+
} catch {
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
function checkEditLoop(cfg, sess, filePath, output) {
|
|
1782
|
+
if (!cfg.enabled) return;
|
|
1783
|
+
const count = (sess.editCounts.get(filePath) ?? 0) + 1;
|
|
1784
|
+
sess.editCounts.set(filePath, count);
|
|
1785
|
+
if (count >= cfg.threshold && count % cfg.threshold === 0) {
|
|
1786
|
+
output.output += `
|
|
1787
|
+
|
|
1788
|
+
--- LOOP WARNING ---
|
|
1789
|
+
You've edited ${path6.basename(filePath)} ${count} times this session. Consider reconsidering your approach \u2014 are you stuck in a loop? Step back and think about whether a different strategy would be more effective.
|
|
1790
|
+
---`;
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
function checkReadDedup(cfg, sess, filePath, output) {
|
|
1794
|
+
if (!cfg.enabled) return false;
|
|
1795
|
+
if (!filePath) return false;
|
|
1796
|
+
const hash = hashContent(output.output);
|
|
1797
|
+
const cached = sess.readCache.get(filePath);
|
|
1798
|
+
if (cached && cached.hash === hash) {
|
|
1799
|
+
output.output = `[File unchanged since tool call #${cached.callSeq}. Content identical (hash: ${hash}). See earlier read for full text.]`;
|
|
1800
|
+
return true;
|
|
1801
|
+
}
|
|
1802
|
+
sess.readCache.set(filePath, { hash, callSeq: sess.callSeq });
|
|
1803
|
+
return false;
|
|
1804
|
+
}
|
|
1805
|
+
var pluginConfig = null;
|
|
1806
|
+
var storedPluginOptions;
|
|
1807
|
+
var plugin5 = async ({ client }, options) => {
|
|
1808
|
+
storedPluginOptions = options;
|
|
1809
|
+
return {
|
|
1810
|
+
config: async (config) => {
|
|
1811
|
+
pluginConfig = resolveConfig(config, storedPluginOptions);
|
|
1812
|
+
},
|
|
1813
|
+
"tool.execute.after": async (input, output) => {
|
|
1814
|
+
const cfg = pluginConfig ?? resolveConfig({}, storedPluginOptions);
|
|
1815
|
+
const sess = getSession(input.sessionID);
|
|
1816
|
+
const toolName = input.tool;
|
|
1817
|
+
if (toolName === "read") {
|
|
1818
|
+
const fp = extractFilePath(input.args);
|
|
1819
|
+
const deduped = checkReadDedup(cfg.readDedup, sess, fp, output);
|
|
1820
|
+
if (deduped) return;
|
|
1821
|
+
}
|
|
1822
|
+
if (EDIT_TOOLS2.has(toolName)) {
|
|
1823
|
+
const fp = extractFilePath(input.args);
|
|
1824
|
+
if (fp) {
|
|
1825
|
+
checkEditLoop(cfg.loopDetection, sess, fp, output);
|
|
1826
|
+
await runPostEditVerify(
|
|
1827
|
+
cfg.verifyLoop,
|
|
1828
|
+
client,
|
|
1829
|
+
sess,
|
|
1830
|
+
input.sessionID,
|
|
1831
|
+
fp,
|
|
1832
|
+
output
|
|
1833
|
+
);
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
applyBackpressure(cfg.backpressure, toolName, input.callID, output, input.args);
|
|
1837
|
+
}
|
|
1838
|
+
};
|
|
1839
|
+
};
|
|
1840
|
+
var tool_hooks_default = plugin5;
|
|
1841
|
+
|
|
1842
|
+
// src/plugins/telemetry.ts
|
|
1843
|
+
import { extname as extname2 } from "path";
|
|
1844
|
+
|
|
1845
|
+
// src/telemetry.ts
|
|
1846
|
+
import { createHash as createHash2, randomUUID } from "crypto";
|
|
1847
|
+
import { homedir as homedir4 } from "os";
|
|
1848
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync } from "fs";
|
|
1849
|
+
import { join as join8 } from "path";
|
|
1850
|
+
var APP_KEY = "A-US-3617699429";
|
|
1851
|
+
var ENDPOINT = "https://us.aptabase.com/api/v0/event";
|
|
1852
|
+
var PKG_NAME = "@glrs-dev/harness-plugin-opencode";
|
|
1853
|
+
var PKG_VERSION = true ? "0.2.0" : "dev";
|
|
1854
|
+
var DISABLED = process.env.HARNESS_OPENCODE_TELEMETRY === "0" || process.env.HARNESS_OPENCODE_TELEMETRY === "false" || process.env.DO_NOT_TRACK === "1" || process.env.CI === "true";
|
|
1855
|
+
var SESSION_ID = randomUUID();
|
|
1856
|
+
function getInstallId() {
|
|
1857
|
+
const dir = join8(homedir4(), ".config", "harness-opencode");
|
|
1858
|
+
const file = join8(dir, "install-id");
|
|
1859
|
+
try {
|
|
1860
|
+
if (existsSync(file)) return readFileSync3(file, "utf8").trim();
|
|
1861
|
+
mkdirSync3(dir, { recursive: true });
|
|
1862
|
+
const id = createHash2("sha256").update(randomUUID()).digest("hex").slice(0, 16);
|
|
1863
|
+
writeFileSync3(file, id, { mode: 384 });
|
|
1864
|
+
return id;
|
|
1865
|
+
} catch {
|
|
1866
|
+
return "anon";
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
var ALLOWED_PROPS = /* @__PURE__ */ new Set([
|
|
1870
|
+
"tool",
|
|
1871
|
+
"outcome",
|
|
1872
|
+
"duration_ms",
|
|
1873
|
+
"edit_kind",
|
|
1874
|
+
"ops_count",
|
|
1875
|
+
"retry_count",
|
|
1876
|
+
"diagnostics_count",
|
|
1877
|
+
"ext",
|
|
1878
|
+
"stale",
|
|
1879
|
+
"error_class",
|
|
1880
|
+
"subagent",
|
|
1881
|
+
"memory_op",
|
|
1882
|
+
"tool_category"
|
|
1883
|
+
]);
|
|
1884
|
+
function clean(p) {
|
|
1885
|
+
const out = {};
|
|
1886
|
+
for (const [k, v] of Object.entries(p)) {
|
|
1887
|
+
if (!ALLOWED_PROPS.has(k)) continue;
|
|
1888
|
+
if (typeof v === "string" || typeof v === "number") out[k] = v;
|
|
1889
|
+
else if (typeof v === "boolean") out[k] = v ? 1 : 0;
|
|
1890
|
+
}
|
|
1891
|
+
return out;
|
|
1892
|
+
}
|
|
1893
|
+
var installId = DISABLED ? "" : getInstallId();
|
|
1894
|
+
function track(eventName, props = {}) {
|
|
1895
|
+
if (DISABLED) return;
|
|
1896
|
+
fetch(ENDPOINT, {
|
|
1897
|
+
method: "POST",
|
|
1898
|
+
headers: {
|
|
1899
|
+
"App-Key": APP_KEY,
|
|
1900
|
+
"Content-Type": "application/json",
|
|
1901
|
+
"User-Agent": `${PKG_NAME}/${PKG_VERSION} node/${process.version}`
|
|
1902
|
+
},
|
|
1903
|
+
body: JSON.stringify({
|
|
1904
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1905
|
+
sessionId: SESSION_ID,
|
|
1906
|
+
eventName,
|
|
1907
|
+
systemProps: {
|
|
1908
|
+
isDebug: process.env.NODE_ENV !== "production",
|
|
1909
|
+
osName: process.platform,
|
|
1910
|
+
osVersion: process.release?.name ?? "node",
|
|
1911
|
+
locale: (process.env.LANG ?? "en").split(".")[0] ?? "en",
|
|
1912
|
+
appVersion: PKG_VERSION,
|
|
1913
|
+
appBuildNumber: PKG_VERSION,
|
|
1914
|
+
sdkVersion: "harness-opencode-fetch@1",
|
|
1915
|
+
engineName: "node",
|
|
1916
|
+
engineVersion: process.version
|
|
1917
|
+
},
|
|
1918
|
+
props: { ...clean(props), install: installId.slice(0, 8) }
|
|
1919
|
+
})
|
|
1920
|
+
}).catch(() => {
|
|
1921
|
+
});
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
// src/plugins/telemetry.ts
|
|
1925
|
+
var plugin6 = async () => {
|
|
1926
|
+
if (DISABLED) {
|
|
1927
|
+
return {};
|
|
1928
|
+
}
|
|
1929
|
+
track("plugin.loaded");
|
|
1930
|
+
const sessionStart = Date.now();
|
|
1931
|
+
let toolCalls = 0;
|
|
1932
|
+
const callTimings = /* @__PURE__ */ new Map();
|
|
1933
|
+
return {
|
|
1934
|
+
"tool.execute.before": async (input, _output) => {
|
|
1935
|
+
callTimings.set(input.callID, Date.now());
|
|
1936
|
+
},
|
|
1937
|
+
"tool.execute.after": async (input, output) => {
|
|
1938
|
+
toolCalls++;
|
|
1939
|
+
const t0 = callTimings.get(input.callID);
|
|
1940
|
+
callTimings.delete(input.callID);
|
|
1941
|
+
const duration_ms = t0 ? Date.now() - t0 : 0;
|
|
1942
|
+
const outStr = String(output.output ?? "");
|
|
1943
|
+
const failed = output.metadata?.error != null || output.metadata?.exitCode != null && output.metadata.exitCode !== 0;
|
|
1944
|
+
const outcome = failed ? "error" : "success";
|
|
1945
|
+
if (input.tool === "hashline_edit") {
|
|
1946
|
+
const filePath = String(input.args?.target_filepath ?? "");
|
|
1947
|
+
const ext = extname2(filePath);
|
|
1948
|
+
const isHashMismatch = /hash.*mismatch|stale/i.test(outStr);
|
|
1949
|
+
const isNotFound = /not.*found/i.test(outStr);
|
|
1950
|
+
track("hashline.edit", {
|
|
1951
|
+
outcome,
|
|
1952
|
+
duration_ms,
|
|
1953
|
+
ext,
|
|
1954
|
+
stale: isHashMismatch,
|
|
1955
|
+
error_class: failed ? isHashMismatch ? "hash_mismatch" : isNotFound ? "file_not_found" : "other" : void 0
|
|
1956
|
+
});
|
|
1957
|
+
} else if (input.tool?.startsWith("serena_")) {
|
|
1958
|
+
track("serena.call", {
|
|
1959
|
+
tool: input.tool.replace("serena_", ""),
|
|
1960
|
+
duration_ms,
|
|
1961
|
+
outcome
|
|
1962
|
+
});
|
|
1963
|
+
} else if (input.tool?.startsWith("memory_")) {
|
|
1964
|
+
track("memory.call", {
|
|
1965
|
+
memory_op: input.tool.replace("memory_", ""),
|
|
1966
|
+
duration_ms,
|
|
1967
|
+
outcome
|
|
1968
|
+
});
|
|
1969
|
+
} else {
|
|
1970
|
+
track("tool.call", {
|
|
1971
|
+
tool: input.tool,
|
|
1972
|
+
duration_ms,
|
|
1973
|
+
outcome
|
|
1974
|
+
});
|
|
1975
|
+
}
|
|
1976
|
+
},
|
|
1977
|
+
event: async ({ event }) => {
|
|
1978
|
+
if (event.type === "session.idle") {
|
|
1979
|
+
track("session.ended", {
|
|
1980
|
+
duration_ms: Date.now() - sessionStart,
|
|
1981
|
+
ops_count: toolCalls
|
|
1982
|
+
});
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
};
|
|
1986
|
+
};
|
|
1987
|
+
var telemetry_default = plugin6;
|
|
1988
|
+
|
|
1989
|
+
// src/index.ts
|
|
1990
|
+
var BUNDLED_VERSION = readOurPackageVersion(import.meta.url);
|
|
1991
|
+
async function checkForUpdate(client) {
|
|
1992
|
+
if (process.env["HARNESS_OPENCODE_UPDATE_CHECK"] === "0") return;
|
|
1993
|
+
try {
|
|
1994
|
+
const controller = new AbortController();
|
|
1995
|
+
const timer = setTimeout(() => controller.abort(), 3e3);
|
|
1996
|
+
const res = await fetch(
|
|
1997
|
+
`https://registry.npmjs.org/${PACKAGE_NAME}/latest`,
|
|
1998
|
+
{ signal: controller.signal }
|
|
1999
|
+
);
|
|
2000
|
+
clearTimeout(timer);
|
|
2001
|
+
if (!res.ok) return;
|
|
2002
|
+
const data = await res.json();
|
|
2003
|
+
const latest = data.version;
|
|
2004
|
+
if (latest && latest !== BUNDLED_VERSION) {
|
|
2005
|
+
const refresh = await refreshPluginCache(BUNDLED_VERSION, latest).catch(
|
|
2006
|
+
(err) => ({
|
|
2007
|
+
outcome: "error",
|
|
2008
|
+
message: err.message,
|
|
2009
|
+
fromVersion: BUNDLED_VERSION,
|
|
2010
|
+
toVersion: latest
|
|
2011
|
+
})
|
|
2012
|
+
);
|
|
2013
|
+
const toastMessage = refresh.outcome === "refreshed" ? `You have ${BUNDLED_VERSION}. Next OpenCode restart will auto-update.` : refresh.outcome === "disabled" ? `You have ${BUNDLED_VERSION}. Auto-update disabled; restart to pick up the new version (cache may need refresh).` : refresh.outcome === "non-exact-pin" ? `You have ${BUNDLED_VERSION}. Cache uses a custom version spec \u2014 run: bun update ${PACKAGE_NAME}` : (
|
|
2014
|
+
// cache-missing / not-our-package / already-current / error
|
|
2015
|
+
`You have ${BUNDLED_VERSION}. Restart OpenCode to refresh (${refresh.outcome}).`
|
|
2016
|
+
);
|
|
2017
|
+
try {
|
|
2018
|
+
await client.tui.showToast({
|
|
2019
|
+
body: {
|
|
2020
|
+
title: `${PACKAGE_NAME} ${latest} available`,
|
|
2021
|
+
message: toastMessage,
|
|
2022
|
+
variant: "info",
|
|
2023
|
+
duration: 8e3
|
|
2024
|
+
}
|
|
2025
|
+
});
|
|
2026
|
+
} catch {
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
} catch {
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
var plugin7 = async (input, options) => {
|
|
2033
|
+
const pluginOptions = options ?? {};
|
|
2034
|
+
loadDotenv(input.directory);
|
|
2035
|
+
checkForUpdate(input.client).catch(() => {
|
|
2036
|
+
});
|
|
2037
|
+
const autopilotHooks = await autopilot_default(input);
|
|
2038
|
+
const notifyHooks = await notify_default(input);
|
|
2039
|
+
const costTrackerHooks = await cost_tracker_default(input);
|
|
2040
|
+
const pilotHooks = await pilot_plugin_default(input);
|
|
2041
|
+
const toolHooks = await tool_hooks_default(input, pluginOptions);
|
|
2042
|
+
const telemetryHooks = await telemetry_default(input);
|
|
2043
|
+
const hooks = {
|
|
2044
|
+
// Config hook: register agents, commands, MCPs, skills
|
|
2045
|
+
config: async (config) => {
|
|
2046
|
+
applyConfig(config, pluginOptions);
|
|
2047
|
+
if (autopilotHooks.config) await autopilotHooks.config(config);
|
|
2048
|
+
if (notifyHooks.config) await notifyHooks.config(config);
|
|
2049
|
+
if (costTrackerHooks.config) await costTrackerHooks.config(config);
|
|
2050
|
+
if (toolHooks.config) await toolHooks.config(config);
|
|
2051
|
+
},
|
|
2052
|
+
// Custom tools
|
|
2053
|
+
tool: createTools(),
|
|
2054
|
+
// Event handlers from sub-plugins
|
|
2055
|
+
event: async (input2) => {
|
|
2056
|
+
if (autopilotHooks.event) await autopilotHooks.event(input2);
|
|
2057
|
+
if (notifyHooks.event) await notifyHooks.event(input2);
|
|
2058
|
+
if (costTrackerHooks.event) await costTrackerHooks.event(input2);
|
|
2059
|
+
if (telemetryHooks.event) await telemetryHooks.event(input2);
|
|
2060
|
+
}
|
|
2061
|
+
};
|
|
2062
|
+
if (autopilotHooks["chat.params"] !== void 0) {
|
|
2063
|
+
hooks["chat.params"] = autopilotHooks["chat.params"];
|
|
2064
|
+
}
|
|
2065
|
+
if (autopilotHooks["chat.message"] !== void 0) {
|
|
2066
|
+
hooks["chat.message"] = autopilotHooks["chat.message"];
|
|
2067
|
+
}
|
|
2068
|
+
if (autopilotHooks["experimental.session.compacting"] !== void 0) {
|
|
2069
|
+
hooks["experimental.session.compacting"] = autopilotHooks["experimental.session.compacting"];
|
|
2070
|
+
}
|
|
2071
|
+
const hasTelemetryBefore = telemetryHooks["tool.execute.before"] !== void 0;
|
|
2072
|
+
const hasPilotBefore = pilotHooks["tool.execute.before"] !== void 0;
|
|
2073
|
+
if (hasTelemetryBefore || hasPilotBefore) {
|
|
2074
|
+
hooks["tool.execute.before"] = async (input2, output) => {
|
|
2075
|
+
if (hasTelemetryBefore) await telemetryHooks["tool.execute.before"](input2, output);
|
|
2076
|
+
if (hasPilotBefore) await pilotHooks["tool.execute.before"](input2, output);
|
|
2077
|
+
};
|
|
2078
|
+
}
|
|
2079
|
+
const hasToolHooksAfter = toolHooks["tool.execute.after"] !== void 0;
|
|
2080
|
+
const hasTelemetryAfter = telemetryHooks["tool.execute.after"] !== void 0;
|
|
2081
|
+
if (hasToolHooksAfter || hasTelemetryAfter) {
|
|
2082
|
+
hooks["tool.execute.after"] = async (input2, output) => {
|
|
2083
|
+
if (hasToolHooksAfter) await toolHooks["tool.execute.after"](input2, output);
|
|
2084
|
+
if (hasTelemetryAfter) await telemetryHooks["tool.execute.after"](input2, output);
|
|
2085
|
+
};
|
|
2086
|
+
}
|
|
2087
|
+
return hooks;
|
|
2088
|
+
};
|
|
2089
|
+
var src_default = plugin7;
|
|
2090
|
+
export {
|
|
2091
|
+
src_default as default
|
|
2092
|
+
};
|