@skillstate/opencode 2.2.2 → 3.0.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.
Files changed (62) hide show
  1. package/README.md +194 -142
  2. package/dist/feedback.d.ts +178 -0
  3. package/dist/feedback.d.ts.map +1 -0
  4. package/dist/feedback.js +235 -0
  5. package/dist/feedback.js.map +1 -0
  6. package/dist/index.d.ts +58 -2
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +48 -3
  9. package/dist/index.js.map +1 -1
  10. package/dist/mode.d.ts +97 -0
  11. package/dist/mode.d.ts.map +1 -0
  12. package/dist/mode.js +111 -0
  13. package/dist/mode.js.map +1 -0
  14. package/dist/opencode-adapter.d.ts +10 -46
  15. package/dist/opencode-adapter.d.ts.map +1 -1
  16. package/dist/opencode-adapter.js +1 -64
  17. package/dist/opencode-adapter.js.map +1 -1
  18. package/dist/paper-mode.d.ts +421 -0
  19. package/dist/paper-mode.d.ts.map +1 -0
  20. package/dist/paper-mode.js +445 -0
  21. package/dist/paper-mode.js.map +1 -0
  22. package/dist/plugin.d.ts +218 -76
  23. package/dist/plugin.d.ts.map +1 -1
  24. package/dist/plugin.js +867 -232
  25. package/dist/plugin.js.map +1 -1
  26. package/dist/response-sink.d.ts +208 -0
  27. package/dist/response-sink.d.ts.map +1 -0
  28. package/dist/response-sink.js +243 -0
  29. package/dist/response-sink.js.map +1 -0
  30. package/dist/runtime.d.ts +203 -0
  31. package/dist/runtime.d.ts.map +1 -0
  32. package/dist/runtime.js +332 -0
  33. package/dist/runtime.js.map +1 -0
  34. package/dist/session-registry.d.ts +148 -0
  35. package/dist/session-registry.d.ts.map +1 -0
  36. package/dist/session-registry.js +236 -0
  37. package/dist/session-registry.js.map +1 -0
  38. package/dist/spec-loader.d.ts +81 -0
  39. package/dist/spec-loader.d.ts.map +1 -0
  40. package/dist/spec-loader.js +163 -0
  41. package/dist/spec-loader.js.map +1 -0
  42. package/dist/state-store.d.ts +126 -0
  43. package/dist/state-store.d.ts.map +1 -0
  44. package/dist/state-store.js +186 -0
  45. package/dist/state-store.js.map +1 -0
  46. package/dist/step-boundary.d.ts +91 -0
  47. package/dist/step-boundary.d.ts.map +1 -0
  48. package/dist/step-boundary.js +109 -0
  49. package/dist/step-boundary.js.map +1 -0
  50. package/dist/system-hint.d.ts +129 -0
  51. package/dist/system-hint.d.ts.map +1 -0
  52. package/dist/system-hint.js +166 -0
  53. package/dist/system-hint.js.map +1 -0
  54. package/dist/tools.d.ts +148 -0
  55. package/dist/tools.d.ts.map +1 -0
  56. package/dist/tools.js +350 -0
  57. package/dist/tools.js.map +1 -0
  58. package/package.json +3 -2
  59. package/dist/plugin-types.d.ts +0 -71
  60. package/dist/plugin-types.d.ts.map +0 -1
  61. package/dist/plugin-types.js +0 -8
  62. package/dist/plugin-types.js.map +0 -1
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Project state persistence for the OpenCode v2 plugin.
3
+ *
4
+ * ONE store, THREE guarantees:
5
+ *
6
+ * 1. **Per-project addressing.** The state file is derived from the plugin
7
+ * instance's location (`ctx.location.project.canonical`), never from
8
+ * `process.cwd()`. The v1 plugin called `process.cwd()` on every hook;
9
+ * in v2 a single OpenCode server serves many projects, so the process
10
+ * cwd is simply the wrong answer. Two checkouts no longer share state.
11
+ *
12
+ * 2. **Per-session isolation.** A root session owns the shared project
13
+ * file; a sub-agent session owns `agents/<scope>/skillstate.json` (see
14
+ * `stateScopeFor`). Parallel sub-agents therefore never last-writer-win
15
+ * over the main session's notes.
16
+ *
17
+ * 3. **Never lose a write.** Every mutation runs inside the core
18
+ * cross-process lock ({@link withStateLock}) and lands via
19
+ * {@link atomicWriteFile} (temp sibling → fsync → rename), so a crash
20
+ * mid-write can never leave a truncated or interleaved state file. Reads
21
+ * never throw: a missing or corrupt file reads as `{}`.
22
+ *
23
+ * The store is deliberately schema-free. It does not validate against a
24
+ * procedural spec: OpenCode sessions are free-form work, and forcing a
25
+ * fixed `goal`/`progress`/`next_steps` shape is what made the v1 prompt
26
+ * rewrite the user's actual task into a state machine.
27
+ */
28
+ import type { StatePatch } from '@skillstate/core';
29
+ /** A skillstate document: a plain JSON object. */
30
+ export type StateDocument = Record<string, unknown>;
31
+ /** Options for {@link ProjectStateStore}. */
32
+ export interface ProjectStateStoreOptions {
33
+ /**
34
+ * The project directory the state file lives under. Prefer
35
+ * `ctx.location.project.canonical` — the canonical checkout, stable
36
+ * across worktrees.
37
+ */
38
+ directory: string;
39
+ /** Home directory, for the `$HOME` global bucket. Defaults to `os.homedir()`. */
40
+ home?: string;
41
+ }
42
+ /**
43
+ * The keys a patch actually changed, split by direction. Mirrors the
44
+ * `state.patch` MCP contract so behaviour is identical across hosts.
45
+ */
46
+ export interface StateChanges {
47
+ readonly added: readonly string[];
48
+ readonly updated: readonly string[];
49
+ readonly deleted: readonly string[];
50
+ }
51
+ /** Top-level diff between two state documents. Pure. */
52
+ export declare function diffDocuments(before: StateDocument, after: StateDocument): StateChanges;
53
+ /**
54
+ * The state file for one session scope inside one project.
55
+ *
56
+ * `scope === ''` is the shared project file; any other scope is a
57
+ * sub-agent's isolated copy. Delegates to the core resolver (shared with
58
+ * the MCP server, the CLI and the other host adapters) so every host agrees
59
+ * on where a project's state lives.
60
+ */
61
+ export declare function statePathFor(directory: string, scope: string, home?: string): string;
62
+ /**
63
+ * Reads and merges the per-project state documents for a plugin instance.
64
+ *
65
+ * Stateless between calls apart from the filesystem, so it is safe to keep
66
+ * one instance per plugin and to construct it in tests without any global
67
+ * setup.
68
+ */
69
+ export declare class ProjectStateStore {
70
+ private readonly directory;
71
+ private readonly home;
72
+ constructor(options: ProjectStateStoreOptions);
73
+ /** The project directory this store addresses. */
74
+ get projectDirectory(): string;
75
+ /** The absolute state file path for a scope (`''` = shared). */
76
+ pathFor(scope: string): string;
77
+ /**
78
+ * Whether a state file already exists for this scope. The plugin uses
79
+ * this to decide whether to inject the system hint at all — an untouched
80
+ * project must behave exactly like vanilla OpenCode.
81
+ */
82
+ exists(scope: string): boolean;
83
+ /**
84
+ * Read a scope's state. A missing, unreadable or corrupt file reads as
85
+ * `{}` — a best-effort read that never breaks the agent loop.
86
+ */
87
+ read(scope: string): StateDocument;
88
+ /**
89
+ * Apply a patch to a scope's state and persist it.
90
+ *
91
+ * The read, the merge and the write all happen inside the cross-process
92
+ * lock, so two sub-agents writing disjoint keys both survive — neither
93
+ * can clobber the other's write by reading a stale snapshot.
94
+ *
95
+ * Returns the merged document and the top-level change sets. A `null`
96
+ * value in the patch deletes that key (paper ⊕).
97
+ */
98
+ patch(scope: string, patch: StatePatch): Promise<{
99
+ state: StateDocument;
100
+ changes: StateChanges;
101
+ }>;
102
+ /**
103
+ * Fold a sub-agent's document into this scope's document.
104
+ *
105
+ * `keep` decides what happens when both sides set the same scalar:
106
+ * `'existing'` (default) keeps the receiving document's value, `'source'`
107
+ * takes the sub-agent's. Keys only present in the sub-agent are always
108
+ * taken, and `null` in either document is a deletion.
109
+ *
110
+ * The source document is left on disk — the merge is not destructive, so
111
+ * a sub-agent's notes remain inspectable after the fold.
112
+ */
113
+ merge(scope: string, sourceScope: string, keep?: 'existing' | 'source'): Promise<{
114
+ state: StateDocument;
115
+ changes: StateChanges;
116
+ }>;
117
+ }
118
+ /**
119
+ * Fold `source` into `target` under a conflict policy. Pure.
120
+ *
121
+ * Nested plain objects recurse. A `null` on either side is a deletion and
122
+ * wins outright — an explicit "remove this" is not a scalar conflict. Every
123
+ * other scalar conflict follows `keep`.
124
+ */
125
+ export declare function mergeDocuments(target: StateDocument, source: StateDocument, keep: 'existing' | 'source'): StateDocument;
126
+ //# sourceMappingURL=state-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state-store.d.ts","sourceRoot":"","sources":["../src/state-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAaH,OAAO,KAAK,EAAc,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAE/D,kDAAkD;AAClD,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEpD,6CAA6C;AAC7C,MAAM,WAAW,wBAAwB;IACvC;;;;OAIG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB,iFAAiF;IACjF,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC;AAED,wDAAwD;AACxD,wBAAgB,aAAa,CAC3B,MAAM,EAAE,aAAa,EACrB,KAAK,EAAE,aAAa,GACnB,YAAY,CAed;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAC1B,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,IAAI,GAAE,MAAqB,GAC1B,MAAM,CAER;AAED;;;;;;GAMG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAE9B,YAAY,OAAO,EAAE,wBAAwB,EAG5C;IAED,kDAAkD;IAClD,IAAI,gBAAgB,IAAI,MAAM,CAE7B;IAED,gEAAgE;IAChE,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE7B;IAED;;;;OAIG;IACH,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAM7B;IAED;;;OAGG;IACH,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,aAAa,CAQjC;IAED;;;;;;;;;OASG;IACG,KAAK,CACT,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,UAAU,GAChB,OAAO,CAAC;QAAE,KAAK,EAAE,aAAa,CAAC;QAAC,OAAO,EAAE,YAAY,CAAA;KAAE,CAAC,CAY1D;IAED;;;;;;;;;;OAUG;IACG,KAAK,CACT,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,EACnB,IAAI,GAAE,UAAU,GAAG,QAAqB,GACvC,OAAO,CAAC;QAAE,KAAK,EAAE,aAAa,CAAC;QAAC,OAAO,EAAE,YAAY,CAAA;KAAE,CAAC,CAoB1D;CACF;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,aAAa,EACrB,MAAM,EAAE,aAAa,EACrB,IAAI,EAAE,UAAU,GAAG,QAAQ,GAC1B,aAAa,CAsBf"}
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Project state persistence for the OpenCode v2 plugin.
3
+ *
4
+ * ONE store, THREE guarantees:
5
+ *
6
+ * 1. **Per-project addressing.** The state file is derived from the plugin
7
+ * instance's location (`ctx.location.project.canonical`), never from
8
+ * `process.cwd()`. The v1 plugin called `process.cwd()` on every hook;
9
+ * in v2 a single OpenCode server serves many projects, so the process
10
+ * cwd is simply the wrong answer. Two checkouts no longer share state.
11
+ *
12
+ * 2. **Per-session isolation.** A root session owns the shared project
13
+ * file; a sub-agent session owns `agents/<scope>/skillstate.json` (see
14
+ * `stateScopeFor`). Parallel sub-agents therefore never last-writer-win
15
+ * over the main session's notes.
16
+ *
17
+ * 3. **Never lose a write.** Every mutation runs inside the core
18
+ * cross-process lock ({@link withStateLock}) and lands via
19
+ * {@link atomicWriteFile} (temp sibling → fsync → rename), so a crash
20
+ * mid-write can never leave a truncated or interleaved state file. Reads
21
+ * never throw: a missing or corrupt file reads as `{}`.
22
+ *
23
+ * The store is deliberately schema-free. It does not validate against a
24
+ * procedural spec: OpenCode sessions are free-form work, and forcing a
25
+ * fixed `goal`/`progress`/`next_steps` shape is what made the v1 prompt
26
+ * rewrite the user's actual task into a state machine.
27
+ */
28
+ import * as fs from 'node:fs';
29
+ import * as os from 'node:os';
30
+ import * as path from 'node:path';
31
+ import { atomicWriteFile, isPlainObject, mergePatch, migrate, resolveHostStateForCwd, withStateLock, } from '@skillstate/core';
32
+ /** Top-level diff between two state documents. Pure. */
33
+ export function diffDocuments(before, after) {
34
+ const added = [];
35
+ const updated = [];
36
+ const deleted = [];
37
+ for (const key of Object.keys(after)) {
38
+ if (!(key in before)) {
39
+ added.push(key);
40
+ }
41
+ else if (JSON.stringify(before[key]) !== JSON.stringify(after[key])) {
42
+ updated.push(key);
43
+ }
44
+ }
45
+ for (const key of Object.keys(before)) {
46
+ if (!(key in after))
47
+ deleted.push(key);
48
+ }
49
+ return { added, updated, deleted };
50
+ }
51
+ /**
52
+ * The state file for one session scope inside one project.
53
+ *
54
+ * `scope === ''` is the shared project file; any other scope is a
55
+ * sub-agent's isolated copy. Delegates to the core resolver (shared with
56
+ * the MCP server, the CLI and the other host adapters) so every host agrees
57
+ * on where a project's state lives.
58
+ */
59
+ export function statePathFor(directory, scope, home = os.homedir()) {
60
+ return resolveHostStateForCwd(directory, home, scope);
61
+ }
62
+ /**
63
+ * Reads and merges the per-project state documents for a plugin instance.
64
+ *
65
+ * Stateless between calls apart from the filesystem, so it is safe to keep
66
+ * one instance per plugin and to construct it in tests without any global
67
+ * setup.
68
+ */
69
+ export class ProjectStateStore {
70
+ directory;
71
+ home;
72
+ constructor(options) {
73
+ this.directory = path.resolve(options.directory);
74
+ this.home = options.home ?? os.homedir();
75
+ }
76
+ /** The project directory this store addresses. */
77
+ get projectDirectory() {
78
+ return this.directory;
79
+ }
80
+ /** The absolute state file path for a scope (`''` = shared). */
81
+ pathFor(scope) {
82
+ return statePathFor(this.directory, scope, this.home);
83
+ }
84
+ /**
85
+ * Whether a state file already exists for this scope. The plugin uses
86
+ * this to decide whether to inject the system hint at all — an untouched
87
+ * project must behave exactly like vanilla OpenCode.
88
+ */
89
+ exists(scope) {
90
+ try {
91
+ return fs.statSync(this.pathFor(scope)).isFile();
92
+ }
93
+ catch {
94
+ return false;
95
+ }
96
+ }
97
+ /**
98
+ * Read a scope's state. A missing, unreadable or corrupt file reads as
99
+ * `{}` — a best-effort read that never breaks the agent loop.
100
+ */
101
+ read(scope) {
102
+ const filePath = this.pathFor(scope);
103
+ try {
104
+ const raw = fs.readFileSync(filePath, 'utf-8');
105
+ return migrate(JSON.parse(raw)).state;
106
+ }
107
+ catch {
108
+ return {};
109
+ }
110
+ }
111
+ /**
112
+ * Apply a patch to a scope's state and persist it.
113
+ *
114
+ * The read, the merge and the write all happen inside the cross-process
115
+ * lock, so two sub-agents writing disjoint keys both survive — neither
116
+ * can clobber the other's write by reading a stale snapshot.
117
+ *
118
+ * Returns the merged document and the top-level change sets. A `null`
119
+ * value in the patch deletes that key (paper ⊕).
120
+ */
121
+ async patch(scope, patch) {
122
+ const filePath = this.pathFor(scope);
123
+ const result = await withStateLock(filePath, async () => {
124
+ const before = this.read(scope);
125
+ const after = mergePatch(before, patch);
126
+ await atomicWriteFile(filePath, `${JSON.stringify({ version: 1, state: after }, null, 2)}\n`);
127
+ return { state: after, changes: diffDocuments(before, after) };
128
+ });
129
+ return result;
130
+ }
131
+ /**
132
+ * Fold a sub-agent's document into this scope's document.
133
+ *
134
+ * `keep` decides what happens when both sides set the same scalar:
135
+ * `'existing'` (default) keeps the receiving document's value, `'source'`
136
+ * takes the sub-agent's. Keys only present in the sub-agent are always
137
+ * taken, and `null` in either document is a deletion.
138
+ *
139
+ * The source document is left on disk — the merge is not destructive, so
140
+ * a sub-agent's notes remain inspectable after the fold.
141
+ */
142
+ async merge(scope, sourceScope, keep = 'existing') {
143
+ if (sourceScope === scope) {
144
+ throw new Error('skillstate_merge: a sub-agent cannot merge its own state');
145
+ }
146
+ const source = this.read(sourceScope);
147
+ if (Object.keys(source).length === 0) {
148
+ throw new Error(`skillstate_merge: no saved state for sub-agent "${sourceScope}" — nothing to merge`);
149
+ }
150
+ const filePath = this.pathFor(scope);
151
+ return withStateLock(filePath, async () => {
152
+ const before = this.read(scope);
153
+ const after = mergeDocuments(before, source, keep);
154
+ await atomicWriteFile(filePath, `${JSON.stringify({ version: 1, state: after }, null, 2)}\n`);
155
+ return { state: after, changes: diffDocuments(before, after) };
156
+ });
157
+ }
158
+ }
159
+ /**
160
+ * Fold `source` into `target` under a conflict policy. Pure.
161
+ *
162
+ * Nested plain objects recurse. A `null` on either side is a deletion and
163
+ * wins outright — an explicit "remove this" is not a scalar conflict. Every
164
+ * other scalar conflict follows `keep`.
165
+ */
166
+ export function mergeDocuments(target, source, keep) {
167
+ const result = { ...target };
168
+ for (const [key, value] of Object.entries(source)) {
169
+ if (value === null) {
170
+ delete result[key];
171
+ continue;
172
+ }
173
+ if (!(key in result)) {
174
+ result[key] = value;
175
+ continue;
176
+ }
177
+ if (isPlainObject(value) && isPlainObject(result[key])) {
178
+ result[key] = mergeDocuments(result[key], value, keep);
179
+ continue;
180
+ }
181
+ if (keep === 'source')
182
+ result[key] = value;
183
+ }
184
+ return result;
185
+ }
186
+ //# sourceMappingURL=state-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state-store.js","sourceRoot":"","sources":["../src/state-store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,91 @@
1
+ /**
2
+ * The step boundary — §5.1's one observation per step, enforced in code.
3
+ *
4
+ * ── The divergence this fixes ─────────────────────────────────────────────
5
+ *
6
+ * The paper's Algorithm 1 alternates, once per step:
7
+ *
8
+ * Aₜ ← Format(P, Σₜ, Oₜ); resp ← llm(Aₜ); Σₜ₊₁ ← Σₜ ⊕ ΔΣₜ; Oₜ₊₁ ← execute(aₜ)
9
+ *
10
+ * One prompt, one patch, one action, one observation. The model is never
11
+ * given a loop of its own, so it has no opportunity to run twenty things
12
+ * inside one step, and the state cannot lag the work.
13
+ *
14
+ * This integration cannot own the model call or the tool execution — the
15
+ * OpenCode plugin API exposes neither, and that is a real limit rather than a
16
+ * design choice. What it can do is own the *boundary*, and that is the half
17
+ * that was missing. Delegating execution to the host's agent loop handed the
18
+ * model an unbounded inner loop, and it used it: 21 tool calls, 3 text
19
+ * blocks, one state patch written at the end from whatever observation was
20
+ * current. Two models, repeated runs. The state lagged the work, and a model
21
+ * whose running total lags cannot accumulate.
22
+ *
23
+ * The fix uses a capability that is present and unused: `SessionContext.tools`
24
+ * is handed to the `context` hook on *every* model request. So requests
25
+ * alternate. One may act — tools present, the host executes, the observation
26
+ * lands in Oₜ. The next is given no tools at all, and a model that has just
27
+ * acted and is asked again with nothing to call can only answer in text, which
28
+ * is exactly where `state_patch` lives.
29
+ *
30
+ * That is §5.1's alternation with the host standing in for both `llm` and
31
+ * `execute`. It is not a simulation of the loop: the host really runs the
32
+ * action, and the state really moves between steps.
33
+ *
34
+ * ── What it does not do ───────────────────────────────────────────────────
35
+ *
36
+ * It cannot make the model write a *good* patch, only make it answer. The
37
+ * merge, the validation and the retry-with-rollback are unchanged, and a
38
+ * patch that fails validation is still refused. This buys the paper's
39
+ * one-observation-per-step; it does not buy correctness of reasoning.
40
+ */
41
+ export declare class StepBoundary {
42
+ #private;
43
+ /**
44
+ * Decide what a request may do, and remember the answer.
45
+ *
46
+ * Returns true when the model may act. A session starts in `act`, so the
47
+ * first request can do real work; every request after an action is a report.
48
+ */
49
+ mayAct(sessionID: string): boolean;
50
+ /**
51
+ * Record that an action ran, so the next request must report.
52
+ *
53
+ * Called when the host executed a tool on this session's behalf. There is
54
+ * no event that says "a tool finished" in a form the plugin can trust for
55
+ * this, so the boundary is advanced from the patch instead — see
56
+ * {@link reportRequired}. Kept as a separate method so the trigger can
57
+ * change without the cycle changing.
58
+ */
59
+ actionTaken(sessionID: string): void;
60
+ /**
61
+ * Whether this request must be answered with a state patch.
62
+ *
63
+ * The single question the context hook needs. It is also the whole point:
64
+ * the model gets one turn to act and one turn to account for it.
65
+ */
66
+ reportRequired(sessionID: string): boolean;
67
+ /**
68
+ * A patch was applied, so the next request may act again.
69
+ *
70
+ * Only an *applied* patch clears the phase. A rejected one leaves the model
71
+ * in `report`, which is what the retry-with-rollback in §6.3 needs: it is
72
+ * re-asked for the same step rather than being let off the hook to act
73
+ * before it has recorded anything.
74
+ */
75
+ patchApplied(sessionID: string): void;
76
+ /** Reset a session, on teardown or a fresh run. */
77
+ reset(sessionID: string): void;
78
+ /** Forget everything. A plugin unload must not leak a phase map. */
79
+ clear(): void;
80
+ /** How many sessions are mid-cycle, for diagnostics and tests. */
81
+ get size(): number;
82
+ }
83
+ /**
84
+ * Whether an action ends the procedure.
85
+ *
86
+ * Shared with the runtime so the two agree on what "finished" means. They
87
+ * answer the same question from the same string, and two copies of that would
88
+ * be two definitions to keep in step.
89
+ */
90
+ export declare function isTerminalAction(action: string): boolean;
91
+ //# sourceMappingURL=step-boundary.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"step-boundary.d.ts","sourceRoot":"","sources":["../src/step-boundary.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAQH,qBAAa,YAAY;;IAGvB;;;;;OAKG;IACH,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAEjC;IAED;;;;;;;;OAQG;IACH,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAEnC;IAED;;;;;OAKG;IACH,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAEzC;IAED;;;;;;;OAOG;IACH,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAEpC;IAED,mDAAmD;IACnD,KAAK,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAE7B;IAED,oEAAoE;IACpE,KAAK,IAAI,IAAI,CAEZ;IAED,kEAAkE;IAClE,IAAI,IAAI,IAAI,MAAM,CAEjB;CACF;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAExD"}
@@ -0,0 +1,109 @@
1
+ /**
2
+ * The step boundary — §5.1's one observation per step, enforced in code.
3
+ *
4
+ * ── The divergence this fixes ─────────────────────────────────────────────
5
+ *
6
+ * The paper's Algorithm 1 alternates, once per step:
7
+ *
8
+ * Aₜ ← Format(P, Σₜ, Oₜ); resp ← llm(Aₜ); Σₜ₊₁ ← Σₜ ⊕ ΔΣₜ; Oₜ₊₁ ← execute(aₜ)
9
+ *
10
+ * One prompt, one patch, one action, one observation. The model is never
11
+ * given a loop of its own, so it has no opportunity to run twenty things
12
+ * inside one step, and the state cannot lag the work.
13
+ *
14
+ * This integration cannot own the model call or the tool execution — the
15
+ * OpenCode plugin API exposes neither, and that is a real limit rather than a
16
+ * design choice. What it can do is own the *boundary*, and that is the half
17
+ * that was missing. Delegating execution to the host's agent loop handed the
18
+ * model an unbounded inner loop, and it used it: 21 tool calls, 3 text
19
+ * blocks, one state patch written at the end from whatever observation was
20
+ * current. Two models, repeated runs. The state lagged the work, and a model
21
+ * whose running total lags cannot accumulate.
22
+ *
23
+ * The fix uses a capability that is present and unused: `SessionContext.tools`
24
+ * is handed to the `context` hook on *every* model request. So requests
25
+ * alternate. One may act — tools present, the host executes, the observation
26
+ * lands in Oₜ. The next is given no tools at all, and a model that has just
27
+ * acted and is asked again with nothing to call can only answer in text, which
28
+ * is exactly where `state_patch` lives.
29
+ *
30
+ * That is §5.1's alternation with the host standing in for both `llm` and
31
+ * `execute`. It is not a simulation of the loop: the host really runs the
32
+ * action, and the state really moves between steps.
33
+ *
34
+ * ── What it does not do ───────────────────────────────────────────────────
35
+ *
36
+ * It cannot make the model write a *good* patch, only make it answer. The
37
+ * merge, the validation and the retry-with-rollback are unchanged, and a
38
+ * patch that fails validation is still refused. This buys the paper's
39
+ * one-observation-per-step; it does not buy correctness of reasoning.
40
+ */
41
+ /** Actions that end the procedure rather than continuing it. */
42
+ const TERMINAL = new Set(['done', 'complete', 'completed', 'finished', 'stop', 'end', '']);
43
+ export class StepBoundary {
44
+ #phase = new Map();
45
+ /**
46
+ * Decide what a request may do, and remember the answer.
47
+ *
48
+ * Returns true when the model may act. A session starts in `act`, so the
49
+ * first request can do real work; every request after an action is a report.
50
+ */
51
+ mayAct(sessionID) {
52
+ return (this.#phase.get(sessionID) ?? 'act') === 'act';
53
+ }
54
+ /**
55
+ * Record that an action ran, so the next request must report.
56
+ *
57
+ * Called when the host executed a tool on this session's behalf. There is
58
+ * no event that says "a tool finished" in a form the plugin can trust for
59
+ * this, so the boundary is advanced from the patch instead — see
60
+ * {@link reportRequired}. Kept as a separate method so the trigger can
61
+ * change without the cycle changing.
62
+ */
63
+ actionTaken(sessionID) {
64
+ this.#phase.set(sessionID, 'report');
65
+ }
66
+ /**
67
+ * Whether this request must be answered with a state patch.
68
+ *
69
+ * The single question the context hook needs. It is also the whole point:
70
+ * the model gets one turn to act and one turn to account for it.
71
+ */
72
+ reportRequired(sessionID) {
73
+ return (this.#phase.get(sessionID) ?? 'act') === 'report';
74
+ }
75
+ /**
76
+ * A patch was applied, so the next request may act again.
77
+ *
78
+ * Only an *applied* patch clears the phase. A rejected one leaves the model
79
+ * in `report`, which is what the retry-with-rollback in §6.3 needs: it is
80
+ * re-asked for the same step rather than being let off the hook to act
81
+ * before it has recorded anything.
82
+ */
83
+ patchApplied(sessionID) {
84
+ this.#phase.set(sessionID, 'act');
85
+ }
86
+ /** Reset a session, on teardown or a fresh run. */
87
+ reset(sessionID) {
88
+ this.#phase.delete(sessionID);
89
+ }
90
+ /** Forget everything. A plugin unload must not leak a phase map. */
91
+ clear() {
92
+ this.#phase.clear();
93
+ }
94
+ /** How many sessions are mid-cycle, for diagnostics and tests. */
95
+ get size() {
96
+ return this.#phase.size;
97
+ }
98
+ }
99
+ /**
100
+ * Whether an action ends the procedure.
101
+ *
102
+ * Shared with the runtime so the two agree on what "finished" means. They
103
+ * answer the same question from the same string, and two copies of that would
104
+ * be two definitions to keep in step.
105
+ */
106
+ export function isTerminalAction(action) {
107
+ return TERMINAL.has(action.trim().toLowerCase());
108
+ }
109
+ //# sourceMappingURL=step-boundary.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"step-boundary.js","sourceRoot":"","sources":["../src/step-boundary.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAKH,gEAAgE;AAChE,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,UAAU,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC,CAAC;AAE3F,MAAM,OAAO,YAAY;IACd,MAAM,GAAG,IAAI,GAAG,EAAiB,CAAC;IAE3C;;;;;OAKG;IACH,MAAM,CAAC,SAAiB;QACtB,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,KAAK,CAAC,KAAK,KAAK,CAAC;IACzD,CAAC;IAED;;;;;;;;OAQG;IACH,WAAW,CAAC,SAAiB;QAC3B,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;IACvC,CAAC;IAED;;;;;OAKG;IACH,cAAc,CAAC,SAAiB;QAC9B,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,KAAK,CAAC,KAAK,QAAQ,CAAC;IAC5D,CAAC;IAED;;;;;;;OAOG;IACH,YAAY,CAAC,SAAiB;QAC5B,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;IACpC,CAAC;IAED,mDAAmD;IACnD,KAAK,CAAC,SAAiB;QACrB,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IAChC,CAAC;IAED,oEAAoE;IACpE,KAAK;QACH,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;IACtB,CAAC;IAED,kEAAkE;IAClE,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC;IAC1B,CAAC;CACF;AAED;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,MAAc;IAC7C,OAAO,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC;AACnD,CAAC"}
@@ -0,0 +1,129 @@
1
+ /**
2
+ * The system-prompt fragment the plugin contributes to each model request.
3
+ *
4
+ * ── Why this module is so small and so careful ───────────────────────────
5
+ *
6
+ * The v1 integration rewrote the conversation on every turn: it truncated
7
+ * history to the last three messages and appended a synthetic `role: "user"`
8
+ * message whose body was the raw state JSON. The reported symptom was that
9
+ * the agent stopped doing what it was asked and started emitting state
10
+ * JSON instead. The cause was structural, not a model quirk:
11
+ *
12
+ * - The last `role: "user"` message is what the model treats as the current
13
+ * instruction, so the state blob displaced the user's actual request.
14
+ * - Truncating history deleted the task statement, the tool results, and
15
+ * the errors the agent had just been given — it could not reason about
16
+ * work it could no longer see.
17
+ *
18
+ * The fix is a rule, enforced by `tests/opencode/context-integrity.test.ts`:
19
+ * **this plugin never mutates `event.messages`.** It contributes one
20
+ * additive, bounded fragment to `event.system`, states what the tools are
21
+ * for, and explicitly leaves the task alone.
22
+ *
23
+ * The wording below is a product requirement, not prose taste. It must
24
+ * describe the tools without instructing the model to change how it works.
25
+ * Imperative framing ("you must", "always", "respond with a JSON block")
26
+ * is what turns a persistence aid into a prompt override.
27
+ */
28
+ /** Largest state JSON rendered into the hint before falling back to a key list. */
29
+ export declare const MAX_INLINE_STATE_CHARS = 4000;
30
+ /** The tool names the hint advertises — kept in sync with `tools.ts`. */
31
+ export declare const ADVERTISED_TOOLS: readonly ['skillstate_read', 'skillstate_update', 'skillstate_merge'];
32
+ /**
33
+ * Render a state document for inclusion in the system prompt.
34
+ *
35
+ * Small documents are inlined verbatim (the model sees what is already
36
+ * saved without a tool round-trip). Documents past
37
+ * {@link MAX_INLINE_STATE_CHARS} are summarized as a key list plus a count and
38
+ * a pointer to `skillstate_read` — an unbounded state file must not be able to
39
+ * grow the system prompt without limit.
40
+ *
41
+ * The reported count is in the SAME UNIT as the limit, and it is labelled with
42
+ * that unit's name. The first version reported `bytes` next to a limit expressed
43
+ * in characters, which is this project's whole recurring mistake in one line: a
44
+ * state of 4,003 Cyrillic characters is 3,003 over the limit and 7,993 bytes, so
45
+ * the model reading the summary was handed two numbers in two units and a limit
46
+ * in a third. §4.3 is explicit that sizes here are raw string CHARS.
47
+ */
48
+ export declare function renderStateForHint(state: Record<string, unknown>): string;
49
+ /**
50
+ * How many MODEL REQUESTS may pass with the state untouched before the drift
51
+ * notice fires.
52
+ *
53
+ * **"Requests", not "turns".** Measured on the weakest model in the
54
+ * catalogue: 20 file reads took 6 requests, 40 reads took 12, 70 reads took
55
+ * 27. The model batches tool calls, so a request is worth several file reads
56
+ * and a threshold described in "turns" is off by a factor of three. The
57
+ * number here is the unit the hook can actually count.
58
+ *
59
+ * 12 is chosen from the corpus rather than taste: the average run in the
60
+ * host's own store showed the model ~29,900 prompt tokens per request, so a
61
+ * dozen silent requests is roughly 350k tokens re-sent for a record that
62
+ * never moved. High enough that an agent reading five files in a row is not
63
+ * nagged.
64
+ */
65
+ export declare const DRIFT_NOTICE_AFTER_TURNS = 12;
66
+ /** Options for {@link buildStateHint}. */
67
+ export interface StateHintOptions {
68
+ /** The state document this session has saved. */
69
+ state: Record<string, unknown>;
70
+ /** Project-relative state path, for the model's reference. */
71
+ statePath: string;
72
+ /**
73
+ * This session's scope, or `''` when it is a root session. A sub-agent
74
+ * hint names the merge step so the main session can fold its notes back.
75
+ */
76
+ scope?: string;
77
+ /**
78
+ * The fields a project `skill-spec.json` declares, when one is present.
79
+ *
80
+ * Notes mode does not ENFORCE a schema — §4.1 scopes the schema to a spec P,
81
+ * and notes mode has no P because it never formats an A.4 prompt. What it
82
+ * must not do is stay silent, and it was: a project shipped a schema, a model
83
+ * read it, ignored it, and wrote thirty files under a namespace it invented
84
+ * while the declared fields sat at their defaults. Nothing said so, and the
85
+ * state looked populated to a reader checking the wrong key.
86
+ *
87
+ * So the fields are stated, not enforced. A model that follows them writes
88
+ * the state where the spec says; one that does not has been told where the
89
+ * spec says, which is the difference between an override and an oversight.
90
+ */
91
+ declaredFields?: readonly string[];
92
+ /**
93
+ * True when the project is INITIALIZED — a state file exists for it.
94
+ *
95
+ * This is the whole difference between an optional side channel and a
96
+ * record, and the two must not be phrased the same way. Telling a model to
97
+ * "skip these tools when the work needs no memory" is correct advice for a
98
+ * scratch project and an invitation to walk away in a real one, where the
99
+ * user initialized skillstate precisely because the work does need it.
100
+ */
101
+ initialized?: boolean;
102
+ /**
103
+ * Turns taken since the state last changed. Set by the caller, which
104
+ * counts; see {@link DRIFT_NOTICE_AFTER_TURNS}.
105
+ */
106
+ turnsSinceWrite?: number;
107
+ }
108
+ /**
109
+ * The notice shown once when the state has not moved for a long stretch.
110
+ *
111
+ * This is feedback, not an instruction, and the distinction is the whole
112
+ * design. It states a measured fact — this many turns, no write — and
113
+ * nothing about what the model ought to do. That keeps the v1 failure
114
+ * impossible (nothing here can displace the user's task) while still
115
+ * telling a drifting agent that the user initialized this for a reason.
116
+ *
117
+ * It fires once per silence, not every turn: a notice that repeats forever
118
+ * is wallpaper, and after the second copy nobody reads it.
119
+ */
120
+ export declare function driftNotice(turnsSinceWrite: number): string;
121
+ /**
122
+ * Build the system-prompt fragment.
123
+ *
124
+ * Returns `''` for an empty document so an untouched project contributes
125
+ * nothing at all — the plugin is inert until the agent actually saves
126
+ * something, and an inert plugin is indistinguishable from no plugin.
127
+ */
128
+ export declare function buildStateHint(options: StateHintOptions): string;
129
+ //# sourceMappingURL=system-hint.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"system-hint.d.ts","sourceRoot":"","sources":["../src/system-hint.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,mFAAmF;AACnF,eAAO,MAAM,sBAAsB,OAAO,CAAC;AAE3C,yEAAyE;AACzE,eAAO,MAAM,gBAAgB,YAC3B,iBAAiB,EACjB,mBAAmB,EACnB,kBAAkB,CACV,CAAC;AAEX;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAczE;AAED;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,wBAAwB,KAAK,CAAC;AAE3C,0CAA0C;AAC1C,MAAM,WAAW,gBAAgB;IAC/B,iDAAiD;IACjD,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,8DAA8D;IAC9D,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;;;;;;;;;OAaG;IACH,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC;;;;;;;;OAQG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAmBD;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,eAAe,EAAE,MAAM,GAAG,MAAM,CAE3D;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,gBAAgB,GAAG,MAAM,CAkDhE"}