opencode-goal-mode 0.1.0 → 0.2.0

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.
Files changed (53) hide show
  1. package/ARCHITECTURE.md +180 -0
  2. package/README.md +158 -52
  3. package/agents/goal-api-reviewer.md +0 -2
  4. package/agents/goal-architect.md +0 -2
  5. package/agents/goal-commentator.md +0 -2
  6. package/agents/goal-completion-guard.md +0 -2
  7. package/agents/goal-coordinator.md +0 -2
  8. package/agents/goal-data-reviewer.md +0 -2
  9. package/agents/goal-deep-researcher.md +0 -2
  10. package/agents/goal-diff-reviewer.md +0 -2
  11. package/agents/goal-doc-reviewer.md +0 -2
  12. package/agents/goal-doc-writer.md +0 -2
  13. package/agents/goal-explorer.md +9 -8
  14. package/agents/goal-final-auditor.md +0 -2
  15. package/agents/goal-implementer.md +0 -2
  16. package/agents/goal-mapper.md +0 -2
  17. package/agents/goal-ops-reviewer.md +0 -2
  18. package/agents/goal-perf-reviewer.md +0 -2
  19. package/agents/goal-planner.md +10 -5
  20. package/agents/goal-prompt-auditor.md +0 -2
  21. package/agents/goal-quality-gate.md +0 -2
  22. package/agents/goal-researcher.md +8 -7
  23. package/agents/goal-reviewer.md +0 -2
  24. package/agents/goal-security-reviewer.md +0 -2
  25. package/agents/goal-test-reviewer.md +0 -2
  26. package/agents/goal-ux-reviewer.md +0 -2
  27. package/agents/goal-verifier.md +0 -2
  28. package/agents/goal-web-researcher.md +0 -2
  29. package/agents/goal.md +9 -8
  30. package/package.json +13 -9
  31. package/plugins/goal-guard/agents.js +132 -0
  32. package/plugins/goal-guard/completion.js +64 -0
  33. package/plugins/goal-guard/config.js +87 -0
  34. package/plugins/goal-guard/events.js +65 -0
  35. package/plugins/goal-guard/gates.js +85 -0
  36. package/plugins/goal-guard/logger.js +36 -0
  37. package/plugins/goal-guard/persistence.js +122 -0
  38. package/plugins/goal-guard/shell.js +1159 -0
  39. package/plugins/goal-guard/state.js +182 -0
  40. package/plugins/goal-guard/summary.js +46 -0
  41. package/plugins/goal-guard/system.js +43 -0
  42. package/plugins/goal-guard/tools.js +129 -0
  43. package/plugins/goal-guard/verdicts.js +87 -0
  44. package/plugins/goal-guard.js +267 -379
  45. package/scripts/install.mjs +170 -36
  46. package/docs/research-report.md +0 -37
  47. package/scripts/check-npm-publish-ready.mjs +0 -54
  48. package/scripts/validate-opencode-config.mjs +0 -82
  49. package/tests/agents.test.mjs +0 -70
  50. package/tests/commands.test.mjs +0 -23
  51. package/tests/helpers.mjs +0 -23
  52. package/tests/install.test.mjs +0 -64
  53. package/tests/plugin.test.mjs +0 -195
@@ -1,8 +1,6 @@
1
1
  ---
2
2
  description: Use in every Goal Mode review cycle to compare the original user prompt and Goal Contract against the delivered outcome with extreme strictness.
3
3
  mode: subagent
4
- model: ordis/chatgpt/gpt-5.5
5
- variant: xhigh
6
4
  temperature: 0
7
5
  color: error
8
6
  permission:
@@ -1,8 +1,6 @@
1
1
  ---
2
2
  description: Use proactively as the final quality gate before completion. Checks standards compliance, naming, style, deprecations, security hygiene, and project-specific quality rules.
3
3
  mode: subagent
4
- model: ordis/chatgpt/gpt-5.5
5
- variant: xhigh
6
4
  temperature: 0
7
5
  color: error
8
6
  permission:
@@ -1,8 +1,6 @@
1
1
  ---
2
2
  description: Use proactively for deep external/docs/schema/API research, unfamiliar dependencies, OpenCode/Claude/Codex behavior, and best-practice investigation.
3
3
  mode: subagent
4
- model: ordis/chatgpt/gpt-5.5
5
- variant: high
6
4
  color: info
7
5
  permission:
8
6
  read: allow
@@ -26,9 +24,12 @@ permission:
26
24
 
27
25
  You are a deep research agent for Goal Mode. Prefer authoritative docs, schemas, source repos, changelogs, and API references. Do not modify files.
28
26
 
29
- Final format:
27
+ Discipline: return distilled findings, not raw dumps. Do not paste long quotes or full pages — summarize and cite. Every non-obvious claim must carry a source.
30
28
 
31
- - Key facts
32
- - Risks or caveats
33
- - Recommended implementation approach
34
- - Sources checked
29
+ Final format (return only these sections):
30
+
31
+ - Key facts: each with the source it came from.
32
+ - Risks or caveats: with severity and why it matters.
33
+ - Recommended implementation approach: concrete and actionable.
34
+ - Confidence: high/medium/low, and what would raise it.
35
+ - Sources checked: URLs or `path:line`.
@@ -1,8 +1,6 @@
1
1
  ---
2
2
  description: Use proactively after implementation for extremely strict correctness, completeness, regression, maintainability, and acceptance-criteria review.
3
3
  mode: subagent
4
- model: ordis/chatgpt/gpt-5.5
5
- variant: xhigh
6
4
  temperature: 0
7
5
  color: error
8
6
  permission:
@@ -1,8 +1,6 @@
1
1
  ---
2
2
  description: Use for auth, secrets, permissions, shell risk, data exposure, destructive actions, network exposure, deployments, and operations risk.
3
3
  mode: subagent
4
- model: ordis/chatgpt/gpt-5.5
5
- variant: xhigh
6
4
  temperature: 0
7
5
  color: error
8
6
  permission:
@@ -1,8 +1,6 @@
1
1
  ---
2
2
  description: Use to identify missing tests, inadequate validation, flaky checks, and better verification commands for Goal Mode.
3
3
  mode: subagent
4
- model: ordis/chatgpt/gpt-5.5
5
- variant: high
6
4
  temperature: 0
7
5
  color: success
8
6
  permission:
@@ -1,8 +1,6 @@
1
1
  ---
2
2
  description: Use for frontend, copy, accessibility, docs, user-facing workflow, CLI usability, and product polish review.
3
3
  mode: subagent
4
- model: ordis/chatgpt/gpt-5.5
5
- variant: high
6
4
  temperature: 0
7
5
  color: accent
8
6
  permission:
@@ -1,8 +1,6 @@
1
1
  ---
2
2
  description: Use to run or plan exact verification commands, summarize outputs, and determine whether evidence proves the Goal is complete.
3
3
  mode: subagent
4
- model: ordis/chatgpt/gpt-5.5
5
- variant: high
6
4
  temperature: 0
7
5
  color: success
8
6
  permission:
@@ -1,8 +1,6 @@
1
1
  ---
2
2
  description: Use proactively for fast web search, page fetching, link summarization, trend checks, package docs, error message lookup, and quick external context without deep analysis.
3
3
  mode: subagent
4
- model: ordis/chatgpt/gpt-5.5
5
- variant: high
6
4
  temperature: 0
7
5
  color: info
8
6
  permission:
package/agents/goal.md CHANGED
@@ -1,8 +1,6 @@
1
1
  ---
2
2
  description: Goal mode for end-to-end autonomous delivery with strict subagent research, implementation ownership, verification, and repeated review cycles until the user's goal is actually complete.
3
3
  mode: primary
4
- model: ordis/chatgpt/gpt-5.5
5
- variant: xhigh
6
4
  color: error
7
5
  permission:
8
6
  read: allow
@@ -40,10 +38,6 @@ permission:
40
38
  doom_loop: allow
41
39
  skill: allow
42
40
  ---
43
- ext_mcp_server_trust:
44
- - github
45
- - browser_automation
46
- - mcp_time
47
41
 
48
42
  You are Goal Mode, an uncompromising autonomous delivery agent. Your job is to finish the user's stated goal, not merely make progress. You have full tool access and must use it responsibly, persistently, and with extreme discipline.
49
43
 
@@ -68,7 +62,6 @@ Delegation rules:
68
62
  - Use `goal-commentator` for improving code comments and annotations.
69
63
  - Use `goal-explorer` and `goal-researcher` for local file discovery and dependency research.
70
64
  - Use `goal-implementer` for bounded implementation subtasks when explicit delegation is safer.
71
- - Use `goal-coordinator` for managing multiple parallel deliverable streams in place of the unavailable `goals` tool.
72
65
  - Use `goal-reviewer` for strict overall correctness and acceptance review.
73
66
  - Use `goal-diff-reviewer` for exact file/code/config diff review.
74
67
  - Use `goal-verifier` for running real verification commands and summarizing evidence.
@@ -76,7 +69,8 @@ Delegation rules:
76
69
  - Use `goal-security-reviewer` for auth, secrets, permissions, network exposure, shell, and destructive risk.
77
70
  - Use `goal-ux-reviewer` for UI, workflow, usability, and accessibility.
78
71
  - Use `goal-doc-reviewer` for documentation quality and accuracy.
79
- - Use `goal-ops-reviewer` for operational, restart, migration, and config-time changes.
72
+ - Use `goal-ops-reviewer` for operational, restart, migration, and config-time changes. When a change is both a security risk and an operational change, run `goal-security-reviewer` for the threat surface and `goal-ops-reviewer` for the rollout/rollback path; they do not substitute for each other.
73
+ - Use `goal-completion-guard` as a fast pre-flight check that every required gate has a fresh PASS before you invoke the final auditor.
80
74
  - Use `goal-final-auditor` as the last gate before `Goal Completed`.
81
75
 
82
76
  Required internal artifacts:
@@ -89,6 +83,13 @@ Required internal artifacts:
89
83
  - Review cycles: N: explicit count of review iterations performed.
90
84
  - Completion Gate: all required reviewers PASS after the latest edit and latest verification.
91
85
 
86
+ Guard tools (provided by the goal-guard plugin):
87
+
88
+ - Call `goal_contract` once the Goal Contract is settled. This activates strict enforcement and tells the guard which specialist review gates your goal requires (security, data, api, perf, etc., inferred from the contract text).
89
+ - Call `goal_evidence` after each meaningful verification run to record the command and result in the Verification Ledger.
90
+ - Call `goal_status` whenever you are unsure what the guard currently requires; it returns the authoritative list of passing, missing, and stale gates and whether completion is allowed. Trust it over your own recollection.
91
+ - The guard injects a live state block into your context each turn and will rewrite a premature `Goal Completed` into `Goal Not Completed` with the missing gates. Use `goal_status` to avoid that rather than guessing.
92
+
92
93
  Context discipline:
93
94
 
94
95
  - Do not fill the main thread with broad search logs, large file summaries, research dumps, or exploratory dead ends.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-goal-mode",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Strict Goal Mode agents, commands, and guard plugin for OpenCode.",
5
5
  "type": "module",
6
6
  "engines": {
@@ -13,19 +13,22 @@
13
13
  "files": [
14
14
  "agents/",
15
15
  "commands/",
16
- "docs/",
17
16
  "plugins/",
18
- "scripts/",
19
- "tests/",
17
+ "scripts/install.mjs",
18
+ "ARCHITECTURE.md",
20
19
  "LICENSE",
21
20
  "README.md"
22
21
  ],
23
22
  "scripts": {
24
23
  "test": "node --test",
25
- "test:agents": "node tests/agents.test.mjs",
26
- "test:commands": "node tests/commands.test.mjs",
27
- "test:plugin": "node tests/plugin.test.mjs",
28
- "test:install": "node tests/install.test.mjs",
24
+ "test:coverage": "node --experimental-test-coverage --test",
25
+ "test:shell": "node --test tests/shell.test.mjs tests/shell.property.test.mjs",
26
+ "test:plugin": "node --test tests/plugin.test.mjs tests/integration.test.mjs",
27
+ "test:unit": "node --test tests/state.test.mjs tests/gates.test.mjs tests/verdicts.test.mjs tests/config.test.mjs tests/persistence.test.mjs",
28
+ "test:agents": "node --test tests/agents.test.mjs tests/commands.test.mjs",
29
+ "test:install": "node --test tests/install.test.mjs",
30
+ "bench": "node benchmarks/run.mjs",
31
+ "bench:compare": "node benchmarks/comparison.mjs",
29
32
  "pack:check": "npm pack --dry-run",
30
33
  "audit": "npm audit --audit-level=moderate",
31
34
  "publish:check": "node scripts/check-npm-publish-ready.mjs",
@@ -56,6 +59,7 @@
56
59
  "registry": "https://registry.npmjs.org/"
57
60
  },
58
61
  "devDependencies": {
59
- "@opencode-ai/plugin": "1.15.13"
62
+ "@opencode-ai/plugin": "1.15.13",
63
+ "fast-check": "^4.8.0"
60
64
  }
61
65
  }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Canonical agent identities and gating maps for Goal Mode.
3
+ *
4
+ * Keeping these in one module means the plugin, the system-prompt injector and
5
+ * the tests all agree on exactly which agents exist, which are review gates,
6
+ * and which contextual keywords pull in which specialist reviewer.
7
+ */
8
+
9
+ /** Read-only review/verification gates that emit `Verdict: PASS|FAIL`. */
10
+ export const REVIEW_AGENTS = Object.freeze([
11
+ "goal-reviewer",
12
+ "goal-prompt-auditor",
13
+ "goal-diff-reviewer",
14
+ "goal-verifier",
15
+ "goal-test-reviewer",
16
+ "goal-security-reviewer",
17
+ "goal-ux-reviewer",
18
+ "goal-ops-reviewer",
19
+ "goal-doc-reviewer",
20
+ "goal-final-auditor",
21
+ "goal-completion-guard",
22
+ "goal-api-reviewer",
23
+ "goal-data-reviewer",
24
+ "goal-perf-reviewer",
25
+ "goal-quality-gate",
26
+ ]);
27
+
28
+ /** Every agent that belongs to Goal Mode (primary + workers + reviewers). */
29
+ export const GOAL_AGENTS = Object.freeze([
30
+ "goal",
31
+ "goal-implementer",
32
+ "goal-explorer",
33
+ "goal-researcher",
34
+ "goal-deep-researcher",
35
+ "goal-web-researcher",
36
+ "goal-architect",
37
+ "goal-mapper",
38
+ "goal-planner",
39
+ "goal-coordinator",
40
+ "goal-doc-writer",
41
+ "goal-commentator",
42
+ ...REVIEW_AGENTS,
43
+ ]);
44
+
45
+ const REVIEW_SET = new Set(REVIEW_AGENTS);
46
+ const GOAL_SET = new Set(GOAL_AGENTS);
47
+
48
+ export function isReviewAgent(name) {
49
+ return REVIEW_SET.has(String(name || ""));
50
+ }
51
+
52
+ export function isGoalAgent(name) {
53
+ return GOAL_SET.has(String(name || ""));
54
+ }
55
+
56
+ /** The primary Goal Mode agent. Only this agent's sessions are "goal sessions"
57
+ * the guard polices for completion — review/worker subagents run in their own
58
+ * child sessions and must not be activated (that would pollute attribution and
59
+ * the active-session population). */
60
+ export const PRIMARY_AGENT = "goal";
61
+
62
+ export function isPrimaryAgent(name) {
63
+ return String(name || "") === PRIMARY_AGENT;
64
+ }
65
+
66
+ /** Reviewers that always run for any meaningful goal. */
67
+ export const BASE_GATES = Object.freeze([
68
+ "goal-prompt-auditor",
69
+ "goal-reviewer",
70
+ "goal-diff-reviewer",
71
+ "goal-verifier",
72
+ "goal-final-auditor",
73
+ ]);
74
+
75
+ /**
76
+ * Keyword → specialist reviewer. When the captured goal text or the set of
77
+ * changed files mentions one of these keywords, the corresponding reviewer
78
+ * becomes a required gate. Keys are matched as whole words against lowercased
79
+ * text so that `api` does not match `capital`.
80
+ */
81
+ export const CONTEXTUAL_GATES = Object.freeze({
82
+ security: "goal-security-reviewer",
83
+ secure: "goal-security-reviewer",
84
+ vulnerability: "goal-security-reviewer",
85
+ secret: "goal-security-reviewer",
86
+ secrets: "goal-security-reviewer",
87
+ password: "goal-security-reviewer",
88
+ credential: "goal-security-reviewer",
89
+ permission: "goal-security-reviewer",
90
+ permissions: "goal-security-reviewer",
91
+ auth: "goal-security-reviewer",
92
+ authentication: "goal-security-reviewer",
93
+ token: "goal-security-reviewer",
94
+ shell: "goal-security-reviewer",
95
+ test: "goal-test-reviewer",
96
+ tests: "goal-test-reviewer",
97
+ coverage: "goal-test-reviewer",
98
+ spec: "goal-test-reviewer",
99
+ ops: "goal-ops-reviewer",
100
+ restart: "goal-ops-reviewer",
101
+ install: "goal-ops-reviewer",
102
+ installer: "goal-ops-reviewer",
103
+ deploy: "goal-ops-reviewer",
104
+ deployment: "goal-ops-reviewer",
105
+ rollback: "goal-ops-reviewer",
106
+ api: "goal-api-reviewer",
107
+ endpoint: "goal-api-reviewer",
108
+ endpoints: "goal-api-reviewer",
109
+ schema: "goal-api-reviewer",
110
+ data: "goal-data-reviewer",
111
+ database: "goal-data-reviewer",
112
+ migration: "goal-data-reviewer",
113
+ migrations: "goal-data-reviewer",
114
+ sql: "goal-data-reviewer",
115
+ performance: "goal-perf-reviewer",
116
+ perf: "goal-perf-reviewer",
117
+ latency: "goal-perf-reviewer",
118
+ throughput: "goal-perf-reviewer",
119
+ scalability: "goal-perf-reviewer",
120
+ ux: "goal-ux-reviewer",
121
+ ui: "goal-ux-reviewer",
122
+ accessibility: "goal-ux-reviewer",
123
+ usability: "goal-ux-reviewer",
124
+ docs: "goal-doc-reviewer",
125
+ documentation: "goal-doc-reviewer",
126
+ readme: "goal-doc-reviewer",
127
+ quality: "goal-quality-gate",
128
+ standards: "goal-quality-gate",
129
+ });
130
+
131
+ /** The reviewer that, when it returns a verdict, closes one review cycle. */
132
+ export const CYCLE_CLOSING_AGENT = "goal-final-auditor";
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Completion-claim enforcement.
3
+ *
4
+ * Evaluates a finished assistant message that claims `Goal Completed` and
5
+ * decides whether the claim is allowed. The rewrite is only applied to ACTIVE
6
+ * goal sessions, so an unrelated assistant turn that merely quotes the phrase
7
+ * in a non-goal session is never corrupted (a bug in the original).
8
+ *
9
+ * A claim is rejected when any of the following hold:
10
+ * - the required `Review cycles: N` line is missing;
11
+ * - no review cycle has been recorded (N must be > 0);
12
+ * - the claimed N does not match the recorded review-cycle count;
13
+ * - required review gates are missing or stale for the current state.
14
+ */
15
+
16
+ import { missingGates, completionAllowed } from "./gates.js";
17
+ import { summarizeState } from "./summary.js";
18
+
19
+ const CYCLES_RE = /Review cycles:\s*(\d+)/i;
20
+
21
+ /**
22
+ * @returns {{ blocked: boolean, reason?: string, replacement?: string, claimedCycles?: number }}
23
+ */
24
+ export function evaluateCompletionClaim(state, config, text) {
25
+ const marker = config.completionMarker || "Goal Completed";
26
+ const escaped = marker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
27
+ // The completion contract requires the message to START with the marker (the
28
+ // final response begins with "Goal Completed"). Anchor to the first non-space
29
+ // of the message or of any line, so a mid-sentence mention is not policed.
30
+ const markerRe = new RegExp(`^[\\s>*_#-]*${escaped}`, "im");
31
+
32
+ if (!text || !markerRe.test(text)) return { blocked: false };
33
+ // Only police active goal sessions.
34
+ if (!state.active) return { blocked: false };
35
+
36
+ const match = text.match(CYCLES_RE);
37
+ const claimedCycles = match ? Number.parseInt(match[1], 10) : -1;
38
+ const summary = summarizeState(state, config);
39
+
40
+ let reason = null;
41
+ if (claimedCycles < 0) {
42
+ reason = "missing required Review cycles line";
43
+ } else if (state.reviewCycles === 0) {
44
+ reason = "no review cycles recorded";
45
+ } else if (claimedCycles !== state.reviewCycles) {
46
+ reason = `claimed review cycles (${claimedCycles}) do not match recorded review cycles (${state.reviewCycles})`;
47
+ } else if (!completionAllowed(state, config)) {
48
+ const missing = missingGates(state, config).join(", ");
49
+ reason = `required review gates are missing or stale (${missing || "goal session not active"})`;
50
+ }
51
+
52
+ if (!reason) return { blocked: false, claimedCycles };
53
+
54
+ const blockedMarker = config.blockedMarker || "Goal Not Completed";
55
+ // Rewrite the SAME anchored line that triggered detection (preserving its
56
+ // leading markdown prefix), not merely the first occurrence of the phrase —
57
+ // otherwise an unrelated earlier mention would be mangled while the real
58
+ // completion-claim heading stayed unflipped.
59
+ const markerLineRe = new RegExp(`^([\\s>*_#-]*)${escaped}`, "im");
60
+ const replacement =
61
+ text.replace(markerLineRe, (_m, prefix) => `${prefix}${blockedMarker}`) +
62
+ `\n\nGoal Guard blocked completion: ${reason}. State: ${summary}`;
63
+ return { blocked: true, reason, replacement, claimedCycles };
64
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Configuration resolution for the guard.
3
+ *
4
+ * Precedence (lowest → highest): built-in defaults < environment variables <
5
+ * the plugin `options` object passed from opencode.json's `["spec", {…}]`
6
+ * form. This lets the guard be tuned per project without code changes, while
7
+ * still working with zero configuration when auto-discovered.
8
+ */
9
+
10
+ export const DEFAULT_CONFIG = Object.freeze({
11
+ /** Block destructive shell commands before execution (throw in tool.execute.before). */
12
+ blockDestructive: true,
13
+ /** Also block remote-code-execution pipelines (curl | sh). */
14
+ blockNetworkExec: true,
15
+ /** Rewrite premature `Goal Completed` claims in experimental.text.complete. */
16
+ enforceCompletion: true,
17
+ /** Inject a live Goal Guard state block into the system prompt. */
18
+ injectSystemState: true,
19
+ /** Persist guard state to disk so it survives OpenCode restarts. */
20
+ persist: true,
21
+ /** Require the contextual specialist gates derived from goal text / changed files. */
22
+ contextualGates: true,
23
+ /** Maximum tracked sessions before LRU eviction. */
24
+ maxSessions: 200,
25
+ /** Idle TTL (ms) after which a session's state may be dropped. 0 disables TTL. */
26
+ sessionTtlMs: 24 * 60 * 60 * 1000,
27
+ /** Emit a TUI toast when completion is blocked. */
28
+ toastOnBlock: true,
29
+ /** Phrase that, at the start of an assistant message, claims completion. */
30
+ completionMarker: "Goal Completed",
31
+ /** Replacement marker when completion is blocked. */
32
+ blockedMarker: "Goal Not Completed",
33
+ });
34
+
35
+ function coerceBool(value, fallback) {
36
+ if (value === undefined || value === null) return fallback;
37
+ if (typeof value === "boolean") return value;
38
+ const s = String(value).trim().toLowerCase();
39
+ if (["1", "true", "yes", "on"].includes(s)) return true;
40
+ if (["0", "false", "no", "off"].includes(s)) return false;
41
+ return fallback;
42
+ }
43
+
44
+ function coerceInt(value, fallback) {
45
+ if (value === undefined || value === null || value === "") return fallback;
46
+ const n = Number.parseInt(String(value), 10);
47
+ return Number.isFinite(n) && n >= 0 ? n : fallback;
48
+ }
49
+
50
+ function fromEnv(env) {
51
+ const out = {};
52
+ const map = {
53
+ GOAL_GUARD_BLOCK_DESTRUCTIVE: ["blockDestructive", coerceBool],
54
+ GOAL_GUARD_BLOCK_NETWORK_EXEC: ["blockNetworkExec", coerceBool],
55
+ GOAL_GUARD_ENFORCE_COMPLETION: ["enforceCompletion", coerceBool],
56
+ GOAL_GUARD_INJECT_SYSTEM_STATE: ["injectSystemState", coerceBool],
57
+ GOAL_GUARD_PERSIST: ["persist", coerceBool],
58
+ GOAL_GUARD_CONTEXTUAL_GATES: ["contextualGates", coerceBool],
59
+ GOAL_GUARD_MAX_SESSIONS: ["maxSessions", coerceInt],
60
+ GOAL_GUARD_SESSION_TTL_MS: ["sessionTtlMs", coerceInt],
61
+ GOAL_GUARD_TOAST_ON_BLOCK: ["toastOnBlock", coerceBool],
62
+ };
63
+ for (const [key, [field, coerce]] of Object.entries(map)) {
64
+ if (env[key] !== undefined) out[field] = coerce(env[key], DEFAULT_CONFIG[field]);
65
+ }
66
+ return out;
67
+ }
68
+
69
+ /**
70
+ * @param {Record<string, unknown>|undefined} options Plugin options (2nd factory arg).
71
+ * @param {Record<string, string|undefined>} [env] Environment (defaults to process.env).
72
+ * @returns {typeof DEFAULT_CONFIG}
73
+ */
74
+ export function resolveConfig(options, env = process.env) {
75
+ const envConfig = fromEnv(env || {});
76
+ const opts = options && typeof options === "object" ? options : {};
77
+ const merged = { ...DEFAULT_CONFIG, ...envConfig };
78
+
79
+ for (const key of Object.keys(DEFAULT_CONFIG)) {
80
+ if (opts[key] === undefined) continue;
81
+ const def = DEFAULT_CONFIG[key];
82
+ if (typeof def === "boolean") merged[key] = coerceBool(opts[key], merged[key]);
83
+ else if (typeof def === "number") merged[key] = coerceInt(opts[key], merged[key]);
84
+ else merged[key] = opts[key];
85
+ }
86
+ return Object.freeze(merged);
87
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * State-mutation primitives shared by the plugin hooks and the custom tools, so
3
+ * that an edit recorded via the `edit` tool, a bash mutation, a `file.edited`
4
+ * event, and a `goal_evidence` tool call all advance the same monotonic seq and
5
+ * dirty bookkeeping in exactly one place.
6
+ */
7
+
8
+ import { CYCLE_CLOSING_AGENT } from "./agents.js";
9
+ import { gatePassedFresh, completionAllowed } from "./gates.js";
10
+
11
+ function trim(arr, max) {
12
+ if (arr.length > max) arr.splice(0, arr.length - max);
13
+ }
14
+
15
+ export function markEdit(store, state, reason) {
16
+ const at = store.nowIso();
17
+ state.active = true;
18
+ state.dirty = true;
19
+ state.lastEditAt = at;
20
+ state.lastEditSeq = store.nextSeq();
21
+ state.dirtyReasons.push(reason || `edit at ${at}`);
22
+ trim(state.dirtyReasons, 50);
23
+ state.updatedAt = at;
24
+ }
25
+
26
+ export function markVerification(store, state) {
27
+ const at = store.nowIso();
28
+ state.verificationSeen = true;
29
+ state.lastVerificationAt = at;
30
+ state.lastVerificationSeq = store.nextSeq();
31
+ state.updatedAt = at;
32
+ }
33
+
34
+ export function markFileChanged(store, state, file) {
35
+ const name = String(file || "").trim();
36
+ if (!name) return;
37
+ if (!state.changedFiles.includes(name)) state.changedFiles.push(name);
38
+ trim(state.changedFiles, 200);
39
+ markEdit(store, state, `file edited: ${name}`);
40
+ }
41
+
42
+ export function recordEvidence(store, state, command, result, criteria) {
43
+ const at = store.nowIso();
44
+ state.evidence.push({
45
+ command: String(command || ""),
46
+ result: String(result || ""),
47
+ criteria: Array.isArray(criteria) ? criteria.slice(0, 50) : [],
48
+ at,
49
+ });
50
+ trim(state.evidence, 100);
51
+ markVerification(store, state);
52
+ state.updatedAt = at;
53
+ }
54
+
55
+ /**
56
+ * When the cycle-closing auditor passes and all gates are fresh, the candidate
57
+ * is genuinely clean: clear the dirty flag so the next answer can complete.
58
+ */
59
+ export function maybeClearDirtyOnFinalPass(state, config) {
60
+ if (!gatePassedFresh(state, CYCLE_CLOSING_AGENT)) return false;
61
+ if (!completionAllowed(state, config)) return false;
62
+ state.dirty = false;
63
+ state.dirtyReasons = [];
64
+ return true;
65
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Required-gate computation and freshness.
3
+ *
4
+ * Fixes versus the original:
5
+ * - Contextual gates actually fire. The previous code computed required gates
6
+ * from `dirtyReasons` only (which never contain meaningful keywords), so the
7
+ * specialist reviewers were dead code. Here gates are derived from the
8
+ * captured goal text, the recorded Goal Contract, and the set of changed
9
+ * files.
10
+ * - Keyword matching is whole-word (token set), so `api` no longer matches
11
+ * inside `capital` and a fingerprint hex can't accidentally pull in a gate.
12
+ * - Freshness uses the monotonic seq counter and is invalidated by EDITS only.
13
+ * Re-running verification after a clean review no longer re-opens the gates.
14
+ */
15
+
16
+ import { BASE_GATES, CONTEXTUAL_GATES } from "./agents.js";
17
+
18
+ function contractText(contract) {
19
+ if (!contract || typeof contract !== "object") return "";
20
+ const parts = [contract.original];
21
+ for (const field of ["requirements", "inferred", "nonGoals", "acceptanceCriteria"]) {
22
+ if (Array.isArray(contract[field])) parts.push(contract[field].join(" "));
23
+ }
24
+ return parts.filter(Boolean).join(" ");
25
+ }
26
+
27
+ /** Tokenize text into a lowercase whole-word set. */
28
+ function wordSet(text) {
29
+ const set = new Set();
30
+ for (const w of String(text || "").toLowerCase().split(/[^a-z0-9]+/)) {
31
+ if (w) set.add(w);
32
+ }
33
+ return set;
34
+ }
35
+
36
+ /** The specialist reviewers implied by the current goal text/contract/files. */
37
+ export function contextualGatesFor(state) {
38
+ const found = [];
39
+ const text = [state.goalText, contractText(state.contract), (state.changedFiles || []).join(" ")].join(" ");
40
+ const words = wordSet(text);
41
+ for (const [keyword, agent] of Object.entries(CONTEXTUAL_GATES)) {
42
+ if (words.has(keyword) && !found.includes(agent)) found.push(agent);
43
+ }
44
+ return found;
45
+ }
46
+
47
+ /**
48
+ * Union the currently-implied contextual gates into the session's sticky set.
49
+ * Gates are sticky so a long session whose rolling goalText buffer later drops
50
+ * the triggering keyword does not silently lose a required specialist review.
51
+ */
52
+ export function refreshStickyGates(state) {
53
+ if (!Array.isArray(state.stickyGates)) state.stickyGates = [];
54
+ for (const agent of contextualGatesFor(state)) {
55
+ if (!state.stickyGates.includes(agent)) state.stickyGates.push(agent);
56
+ }
57
+ return state.stickyGates;
58
+ }
59
+
60
+ /** The reviewers that must PASS for this state, given config. */
61
+ export function requiredGates(state, config) {
62
+ const gates = [...BASE_GATES];
63
+ if (!config || config.contextualGates) {
64
+ const contextual = new Set([...(state.stickyGates || []), ...contextualGatesFor(state)]);
65
+ for (const agent of contextual) {
66
+ if (!gates.includes(agent)) gates.push(agent);
67
+ }
68
+ }
69
+ return gates;
70
+ }
71
+
72
+ /** A gate is satisfied when its latest verdict is PASS and newer than the last edit. */
73
+ export function gatePassedFresh(state, agent) {
74
+ const v = state.latestVerdict[agent];
75
+ if (!v || v.verdict !== "PASS") return false;
76
+ return v.seq > (state.lastEditSeq || 0);
77
+ }
78
+
79
+ export function missingGates(state, config) {
80
+ return requiredGates(state, config).filter((agent) => !gatePassedFresh(state, agent));
81
+ }
82
+
83
+ export function completionAllowed(state, config) {
84
+ return Boolean(state.active) && missingGates(state, config).length === 0;
85
+ }