@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,166 @@
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 const MAX_INLINE_STATE_CHARS = 4000;
30
+ /** The tool names the hint advertises — kept in sync with `tools.ts`. */
31
+ export const ADVERTISED_TOOLS = [
32
+ 'skillstate_read',
33
+ 'skillstate_update',
34
+ 'skillstate_merge',
35
+ ];
36
+ /**
37
+ * Render a state document for inclusion in the system prompt.
38
+ *
39
+ * Small documents are inlined verbatim (the model sees what is already
40
+ * saved without a tool round-trip). Documents past
41
+ * {@link MAX_INLINE_STATE_CHARS} are summarized as a key list plus a count and
42
+ * a pointer to `skillstate_read` — an unbounded state file must not be able to
43
+ * grow the system prompt without limit.
44
+ *
45
+ * The reported count is in the SAME UNIT as the limit, and it is labelled with
46
+ * that unit's name. The first version reported `bytes` next to a limit expressed
47
+ * in characters, which is this project's whole recurring mistake in one line: a
48
+ * state of 4,003 Cyrillic characters is 3,003 over the limit and 7,993 bytes, so
49
+ * the model reading the summary was handed two numbers in two units and a limit
50
+ * in a third. §4.3 is explicit that sizes here are raw string CHARS.
51
+ */
52
+ export function renderStateForHint(state) {
53
+ const json = JSON.stringify(state, null, 2);
54
+ if (json.length <= MAX_INLINE_STATE_CHARS)
55
+ return json;
56
+ const keys = Object.keys(state).sort();
57
+ return `${JSON.stringify({
58
+ _truncated: true,
59
+ chars: json.length,
60
+ keys,
61
+ hint: 'Saved state is large — call skillstate_read to load it.',
62
+ }, null, 2)}`;
63
+ }
64
+ /**
65
+ * How many MODEL REQUESTS may pass with the state untouched before the drift
66
+ * notice fires.
67
+ *
68
+ * **"Requests", not "turns".** Measured on the weakest model in the
69
+ * catalogue: 20 file reads took 6 requests, 40 reads took 12, 70 reads took
70
+ * 27. The model batches tool calls, so a request is worth several file reads
71
+ * and a threshold described in "turns" is off by a factor of three. The
72
+ * number here is the unit the hook can actually count.
73
+ *
74
+ * 12 is chosen from the corpus rather than taste: the average run in the
75
+ * host's own store showed the model ~29,900 prompt tokens per request, so a
76
+ * dozen silent requests is roughly 350k tokens re-sent for a record that
77
+ * never moved. High enough that an agent reading five files in a row is not
78
+ * nagged.
79
+ */
80
+ export const DRIFT_NOTICE_AFTER_TURNS = 12;
81
+ /**
82
+ * The line that says what the state IS, which depends on whether the project
83
+ * was initialized.
84
+ *
85
+ * Neither version is an imperative. Both are statements about the setup, and
86
+ * the difference is that one describes a record the user asked for and the
87
+ * other describes an optional convenience. A model told to skip the tools
88
+ * when it judges them unnecessary will eventually judge a long task
89
+ * unnecessary at exactly the wrong moment; a model told the state is the
90
+ * project's record has a fact to work with.
91
+ */
92
+ function purposeLine(initialized, statePath) {
93
+ return initialized
94
+ ? `This project has an execution state at \`${statePath}\`. It is the project's record: what has been established, decided, and left to do, kept across a context reset or compaction. The conversation is not that record, and it is not kept — the state file is.`
95
+ : `Notes for this project are saved at \`${statePath}\` and survive a context reset or compaction.`;
96
+ }
97
+ /**
98
+ * The notice shown once when the state has not moved for a long stretch.
99
+ *
100
+ * This is feedback, not an instruction, and the distinction is the whole
101
+ * design. It states a measured fact — this many turns, no write — and
102
+ * nothing about what the model ought to do. That keeps the v1 failure
103
+ * impossible (nothing here can displace the user's task) while still
104
+ * telling a drifting agent that the user initialized this for a reason.
105
+ *
106
+ * It fires once per silence, not every turn: a notice that repeats forever
107
+ * is wallpaper, and after the second copy nobody reads it.
108
+ */
109
+ export function driftNotice(turnsSinceWrite) {
110
+ return `Note: this state file has not changed across the last ${turnsSinceWrite} steps of work.`;
111
+ }
112
+ /**
113
+ * Build the system-prompt fragment.
114
+ *
115
+ * Returns `''` for an empty document so an untouched project contributes
116
+ * nothing at all — the plugin is inert until the agent actually saves
117
+ * something, and an inert plugin is indistinguishable from no plugin.
118
+ */
119
+ export function buildStateHint(options) {
120
+ const { state, statePath } = options;
121
+ const scope = options.scope ?? '';
122
+ if (Object.keys(state).length === 0)
123
+ return '';
124
+ // `skillstate_merge` is only meaningful to a sub-agent, which is the one
125
+ // session that cannot call it — so it is hidden from every other scope.
126
+ const tools = ADVERTISED_TOOLS.filter((name) => name !== 'skillstate_merge' || scope !== '');
127
+ const toolLine = `\nTools: ${tools.map((name) => `\`${name}\``).join(', ')}.`;
128
+ const mergeLine = scope === ''
129
+ ? ''
130
+ : '\nThis is a sub-agent session. When you finish, the main session folds your notes back with `skillstate_merge`; write them as if someone else will read them.';
131
+ const initialized = options.initialized === true;
132
+ const turns = options.turnsSinceWrite ?? 0;
133
+ const declared = options.declaredFields ?? [];
134
+ // Only speak when the state and the spec actually DISAGREE. A line naming
135
+ // the declared fields on every turn of every project would cost prompt budget
136
+ // to say something that is true, and the hint has a standing test that it
137
+ // must stay under a tenth of the conversation it rides along with. The case
138
+ // worth spending characters on is the one measured: a model that wrote
139
+ // everything under a namespace it invented while the declared fields sat at
140
+ // their defaults.
141
+ const undeclared = Object.keys(state).filter((k) => !declared.some((d) => d.startsWith(`${k} `)));
142
+ const schemaLine = declared.length === 0 || undeclared.length === 0
143
+ ? ''
144
+ : `\nThis project\'s \`skill-spec.json\` declares the state fields ${declared
145
+ .map((f) => `\`${f}\``)
146
+ .join(', ')}. The state currently holds ${undeclared
147
+ .slice(0, 6)
148
+ .map((k) => `\`${k}\``)
149
+ .join(', ')}, which the spec does not declare. Write declared fields at the TOP LEVEL under their own names; a later reader looking for \`${declared[0].split(' ')[0]}\` will not find it one level down.`;
150
+ const lines = [
151
+ '<skillstate-project-notes>',
152
+ purposeLine(initialized, statePath),
153
+ '',
154
+ renderStateForHint(state),
155
+ `${toolLine}${mergeLine}${schemaLine}`,
156
+ initialized
157
+ ? 'Record what you establish here as you go — plans made, decisions taken, file paths, values read, and what is left — so it is still here after a reset. The notes are not the task: keep doing what the user asked.'
158
+ : 'Use them only to carry facts across turns — plans already made, decisions already taken, file paths, and what is left to do. The notes are a side channel, not the task: keep doing what the user asked, and skip these tools entirely when the work needs no cross-turn memory.',
159
+ ];
160
+ if (initialized && turns >= DRIFT_NOTICE_AFTER_TURNS) {
161
+ lines.push('', driftNotice(turns));
162
+ }
163
+ lines.push('</skillstate-project-notes>');
164
+ return lines.join('\n');
165
+ }
166
+ //# sourceMappingURL=system-hint.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"system-hint.js","sourceRoot":"","sources":["../src/system-hint.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Native OpenCode tools for skillstate.
3
+ *
4
+ * These are the NATIVE tools: the fast path when the host is opencode v2.
5
+ * They carry a real JSON Schema and structured output, where the MCP server
6
+ * (`@skillstate/mcp`, still shipped and still registered) can only offer a
7
+ * JSON-RPC round-trip and untyped text. Both read the same state file, so
8
+ * the choice between them is about speed and typing on one side and reach
9
+ * on the other -- never about which one is correct.
10
+ *
11
+ * The MCP server was once the ONLY option: an opencode v1 plugin could not
12
+ * contribute first-class tools at all.
13
+ *
14
+ * Three tools, deliberately:
15
+ *
16
+ * - `skillstate_read` - what this session has already saved.
17
+ * - `skillstate_update` - merge a patch (the only write path).
18
+ * - `skillstate_merge` - fold sub-agent notes back into the root session.
19
+ *
20
+ * A fourth "delete everything" tool is intentionally absent: `null` in a
21
+ * patch already deletes a key, and a tool whose only job is to erase state
22
+ * is a foot-gun with no upside.
23
+ *
24
+ * ── Result shape ──────────────────────────────────────────────────────────
25
+ *
26
+ * Every tool returns a DISCRIMINATED result: `{ ok: true, ... }` or
27
+ * `{ ok: false, error }`. This is not a stylistic choice. A tool that
28
+ * declares an `output` schema must return a value matching it, so a failure
29
+ * path that returned only text was rejected by the host with "tool did not
30
+ * return its declared output" — a rejected tool call is worse for the agent
31
+ * than a failed one, because it loses the reason. A validation failure is a
32
+ * normal outcome here, and it is modelled as one.
33
+ *
34
+ * SCOPING is automatic. The current session's scope comes from
35
+ * `ToolContext.sessionID` plus the {@link SessionRegistry}, so the model
36
+ * never passes a session id and can never write another agent's file by
37
+ * guessing one.
38
+ */
39
+ import type { ToolEditor } from '@opencode/plugin/promise/tool';
40
+ import type { StatePatch, StateSchema } from '@skillstate/core';
41
+ import type { SessionRegistry } from './session-registry.js';
42
+ import { stateScopeFor } from './session-registry.js';
43
+ import type { ProjectStateStore, StateChanges } from './state-store.js';
44
+ /**
45
+ * Largest serialized patch a single `skillstate_update` accepts. The state
46
+ * file is a side channel for a human to read; a model that tries to dump a
47
+ * whole file into it should be told, not silently accommodated.
48
+ */
49
+ export declare const MAX_PATCH_BYTES: number;
50
+ /** A tool call that succeeded. */
51
+ export interface ToolOk<T> {
52
+ readonly ok: true;
53
+ readonly value: T;
54
+ }
55
+ /** A tool call that was refused. `error` is written for the model to read. */
56
+ export interface ToolError {
57
+ readonly ok: false;
58
+ readonly error: string;
59
+ }
60
+ /** Every tool result is one of these two. */
61
+ export type ToolResult<T> = ToolOk<T> | ToolError;
62
+ /** Payload of a successful `skillstate_read`. */
63
+ export interface ReadValue {
64
+ readonly state: Record<string, unknown>;
65
+ readonly path: string;
66
+ readonly scope: string;
67
+ readonly empty: boolean;
68
+ }
69
+ /** Payload of a successful `skillstate_update`. */
70
+ export interface UpdateValue {
71
+ readonly state: Record<string, unknown>;
72
+ readonly changes: StateChanges;
73
+ readonly path: string;
74
+ readonly scope: string;
75
+ }
76
+ /** Payload of a successful `skillstate_merge`. */
77
+ export interface MergeValue {
78
+ readonly state: Record<string, unknown>;
79
+ readonly changes: StateChanges;
80
+ readonly merged: readonly string[];
81
+ readonly skipped: readonly string[];
82
+ }
83
+ /** Everything the tool definitions need, bound to one plugin instance. */
84
+ export interface ToolDeps {
85
+ readonly store: ProjectStateStore;
86
+ readonly sessions: SessionRegistry;
87
+ /** Resolve a session id to its state scope; `''` is the root session. */
88
+ scopeFor: (sessionID: string) => string;
89
+ /**
90
+ * The project's declared schema, when it SHIPPED one.
91
+ *
92
+ * `undefined` is the normal case and the correct one: §6.2 validates
93
+ * `ΔΣ_t` against `P.schema`, so there is nothing to check a patch against
94
+ * for a project that has not declared a schema, and inventing a default here
95
+ * would reject notes that are perfectly reasonable.
96
+ *
97
+ * It is passed because this tool is the same file the paper's Σ lives in, and
98
+ * §9.3 says "a conforming writer must never emit a `state` containing a key
99
+ * absent from the schema". Measured: a notes-mode run called this tool
100
+ * thirty-one times from inside the host's `execute` sandbox, and the state
101
+ * file ended with a `last` key that the schema does not declare — accepted,
102
+ * `ok: true`, and written by an unvalidated second writer into the same
103
+ * document the paper's runtime owns. Only a spec the project SHIPPED is used,
104
+ * for the same reason `declaredFields` is gated on the source: announcing our
105
+ * own fallback schema as the project's would be a false claim.
106
+ */
107
+ readonly schema?: StateSchema;
108
+ /**
109
+ * Called after a patch is written, with the scope it was written to.
110
+ *
111
+ * The drift counter resets on it, and the counter is what the system prompt
112
+ * says out loud: "this state file has not changed across the last N steps of
113
+ * work". So it has to be reset by *every* write path, not just the one the
114
+ * paper-mode sink owns.
115
+ *
116
+ * Measured: in notes mode there is no sink, `stateWrites` never increments and
117
+ * `turnsSinceWrite` is never reset — so the notice fires at its threshold and
118
+ * reports silence for a run in which the model wrote the state on every step.
119
+ * The drift measurement that "the notice does not work" rested on was reading
120
+ * a counter that could not see the writes it was counting.
121
+ */
122
+ readonly onWrite?: (scope: string) => void;
123
+ }
124
+ /**
125
+ * Validate and normalize an incoming patch.
126
+ *
127
+ * Returns either the patch or a human-readable reason it was rejected. The
128
+ * checks protect the file; they do not dictate the model's schema. A state
129
+ * note is free-form by design, so the only hard rules are "must be an
130
+ * object", "must be JSON-serializable", and "must fit".
131
+ */
132
+ export declare function normalizePatch(raw: unknown): {
133
+ ok: true;
134
+ patch: StatePatch;
135
+ } | {
136
+ ok: false;
137
+ reason: string;
138
+ };
139
+ /**
140
+ * Register the skillstate tools on an editor.
141
+ *
142
+ * The callback is synchronous, side-effect-free and replayable: OpenCode
143
+ * re-runs it on every registry rebuild, so it must not do I/O at
144
+ * registration time. All filesystem work happens inside `execute`.
145
+ */
146
+ export declare function registerTools(editor: ToolEditor, deps: ToolDeps): void;
147
+ export { stateScopeFor };
148
+ //# sourceMappingURL=tools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH,OAAO,KAAK,EAAe,UAAU,EAAE,MAAM,+BAA+B,CAAC;AAE7E,OAAO,KAAK,EAAc,UAAU,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC5E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,KAAK,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAExE;;;;GAIG;AACH,eAAO,MAAM,eAAe,QAAY,CAAC;AAEzC,kCAAkC;AAClC,MAAM,WAAW,MAAM,CAAC,CAAC;IACvB,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAClB,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC;CACnB;AAED,8EAA8E;AAC9E,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IACnB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,6CAA6C;AAC7C,MAAM,MAAM,UAAU,CAAC,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC;AAElD,iDAAiD;AACjD,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACxC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACzB;AAED,mDAAmD;AACnD,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACxC,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,kDAAkD;AAClD,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACxC,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC;IAC/B,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC;AAED,0EAA0E;AAC1E,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,KAAK,EAAE,iBAAiB,CAAC;IAClC,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAC;IACnC,yEAAyE;IACzE,QAAQ,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,CAAC;IACxC;;;;;;;;;;;;;;;;;OAiBG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAC9B;;;;;;;;;;;;;OAaG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CAC5C;AA0JD;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAC5B,GAAG,EAAE,OAAO,GACX;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,UAAU,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAoCjE;AAwBD;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,QAAQ,GAAG,IAAI,CAqJtE;AAOD,OAAO,EAAE,aAAa,EAAE,CAAC"}