opencode-thrum-pro 0.1.1 → 0.1.3

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 (26) hide show
  1. package/assets/commands/thrum-_stamp-protocol.md +65 -0
  2. package/assets/skills/brainstorm-steward/SKILL.md +15 -0
  3. package/assets/skills/choosing-subagent-models/SKILL.md +19 -11
  4. package/assets/skills/conversation-only-context/SKILL.md +186 -0
  5. package/assets/skills/coordinator-dispatching-work/SKILL.md +28 -8
  6. package/assets/skills/coordinator-hotpath-merge-gate/SKILL.md +68 -0
  7. package/assets/skills/coordinator-merging-code/SKILL.md +24 -0
  8. package/assets/skills/coordinator-running-brainstorm-cycles/SKILL.md +11 -7
  9. package/assets/skills/efficient-multi-agent-research/SKILL.md +7 -6
  10. package/assets/skills/persistent-watcher-archetype/SKILL.md +80 -15
  11. package/assets/skills/persistent-watcher-archetype/resources/thrum-capture-fallback.sh +123 -0
  12. package/assets/skills/persistent-watcher-archetype/resources/thrum-watch-pane-capture.sh +379 -0
  13. package/assets/skills/prime-agent/SKILL.md +2 -4
  14. package/assets/skills/project-setup/SKILL.md +118 -21
  15. package/assets/skills/update-queue/SKILL.md +3 -1
  16. package/assets/thrum_allowlist.json +39 -0
  17. package/dist/allowlist-sync.test.d.ts +1 -0
  18. package/dist/allowlist-sync.test.js +44 -0
  19. package/dist/index.js +24 -1
  20. package/dist/installer.d.ts +29 -0
  21. package/dist/installer.js +356 -1
  22. package/dist/installer.permissions.test.d.ts +1 -0
  23. package/dist/installer.permissions.test.js +602 -0
  24. package/dist/permission-fixture.integration.test.d.ts +1 -0
  25. package/dist/permission-fixture.integration.test.js +506 -0
  26. package/package.json +3 -2
@@ -44,7 +44,45 @@ grep -E 'verify=Ready:Yes([[:space:]]|-->|$)' "$PLAN_FILE" \
44
44
  || grep -E 'verify=PREDATES([[:space:]]|-->|$)' "$PLAN_FILE"
45
45
  ```
46
46
 
47
- All three checks must pass.
47
+ **Fourth check — the stamp must BIND, not just be PRESENT.** A stamp's
48
+ presence proves a review happened; it does not prove the stamp still
49
+ describes the artifact in front of you. Parse the binding keys from the
50
+ **highest-cycle** (last-appended) `stage=plan` line — stamps are only ever
51
+ appended, never reordered, so `tail -1` is the binding verdict; never `sort`
52
+ (a `cycle=10` line must beat `cycle=9`, which a lexical sort inverts) — then
53
+ re-derive the current values per `_stamp-protocol.md` § "Review-object
54
+ binding" and compare:
55
+
56
+ ```bash
57
+ # SOURCE_FILE = the LOCKED brainstorm (+ design spec, if any) this plan was reviewed against
58
+ LINE=$(grep '^<!-- THRUM-REVIEW: stage=plan ' "$PLAN_FILE" | tail -1)
59
+
60
+ # An OVERRIDE on this line already satisfies the fourth check (same escape as
61
+ # the first check) — short-circuit BEFORE the missing-keys exit below, or a
62
+ # legitimate OVERRIDE stamp that omits plan_obj/src_lock is wrongly rejected.
63
+ if ! grep -qE 'verdict=OVERRIDE([[:space:]]|-->|$)' <<<"$LINE"; then
64
+ STAMPED_OBJ=$(sed -n 's/.*plan_obj=\([0-9a-f]\{40,64\}\).*/\1/p' <<<"$LINE")
65
+ STAMPED_LOCK=$(sed -n 's/.*src_lock=\([0-9a-f]\{40,64\}\).*/\1/p' <<<"$LINE")
66
+
67
+ # Missing keys fails exactly like a missing verify= — no silent-omission path.
68
+ [ -n "$STAMPED_OBJ" ] && [ -n "$STAMPED_LOCK" ] || {
69
+ echo "review stamp missing plan_obj/src_lock binding keys" >&2; exit 1; }
70
+
71
+ CURRENT_OBJ=$(grep -v '^<!-- THRUM-REVIEW:' "$PLAN_FILE" | git hash-object --no-filters --stdin)
72
+ CURRENT_LOCK=$(git hash-object --no-filters "$SOURCE_FILE")
73
+
74
+ [ "$STAMPED_OBJ" = "$CURRENT_OBJ" ] \
75
+ || { echo "review stamp does not bind the current plan object" >&2; exit 1; }
76
+ [ "$STAMPED_LOCK" = "$CURRENT_LOCK" ] \
77
+ || { echo "review stamp bound to a superseded source LOCK" >&2; exit 1; }
78
+ fi
79
+ ```
80
+
81
+ An OVERRIDE on the matched `stage=plan` line already satisfies this fourth
82
+ check (same escape as the first check) — the snippet above short-circuits
83
+ before re-deriving or comparing in that case.
84
+
85
+ All four checks must pass.
48
86
 
49
87
  Notes on the check:
50
88
 
@@ -75,29 +113,42 @@ Notes on the check:
75
113
  `OVERRIDE`. This is what makes `verify-against-source` mandatory in fact,
76
114
  not just in name: a plan could otherwise carry `verdict=Ready:Yes` from
77
115
  some OTHER reviewer while `verify-against-source` itself never ran.
116
+ - **The binding check fails closed on either mismatch, with no
117
+ silent-omission path** — a stamp missing `plan_obj`/`src_lock` entirely
118
+ fails exactly like a missing `verify=` field (fourth check above). The
119
+ remedy is the same shape as the `verify=` remedy: re-run the plan review so
120
+ the stamp is emitted with live binding keys (`_stamp-protocol.md` § "Review-
121
+ object binding"), or a coordinator adds an explicit `verdict=OVERRIDE`
122
+ (with `reason=`) to the `stage=plan` line — the SAME auditable escape used
123
+ for the first check, not a new mechanism. There is no `PREDATES` value for
124
+ the binding keys themselves; a legacy plan without them either gets
125
+ re-reviewed (which produces the keys) or overridden.
78
126
 
79
127
  If any check fails, **STOP — do not proceed.** Tell the user:
80
128
 
81
129
  > project-setup requires the plan/spec to have passed the planning-loop review
82
130
  > (or carry a coordinator override), to carry a `## Deviations from Source`
83
- > block, AND to carry `verify=Ready:Yes`, `verify=OVERRIDE`, or
84
- > `verify=PREDATES` evidence that `verify-against-source` specifically ran (or
85
- > was deliberately waived, or the plan predates the convention). The plan at
86
- > `<PLAN_FILE>` is missing one or more of: the review stamp, the Deviations
87
- > block, the `verify=` field — check which with the three `grep -F` commands
88
- > above. Run the plan review first (making sure `verify-against-source` runs
89
- > and stamps `verify=`), obtain a coordinator override, or stamp
90
- > `verify=PREDATES` if the plan predates this convention, then re-invoke
91
- > project-setup.
131
+ > block, to carry `verify=Ready:Yes`, `verify=OVERRIDE`, or `verify=PREDATES`
132
+ > evidence that `verify-against-source` specifically ran (or was deliberately
133
+ > waived, or the plan predates the convention), AND for the review stamp to
134
+ > BIND the current plan object and current locked source (`plan_obj`/
135
+ > `src_lock`). The plan at `<PLAN_FILE>` is missing one or more of: the review
136
+ > stamp, the Deviations block, the `verify=` field, or a valid binding — check
137
+ > which with the commands above. Run the plan review first (making sure
138
+ > `verify-against-source` runs, `verify=` is stamped, and `plan_obj`/
139
+ > `src_lock` are computed fresh), obtain a coordinator override, or stamp
140
+ > `verify=PREDATES` if the plan predates the `verify=` convention, then
141
+ > re-invoke project-setup.
92
142
 
93
143
  **Fail closed, not open.** Plans that predate this feature, or that arrive from
94
- a different flow, will not carry the stamp, the Deviations block, or the
95
- `verify=` field — they still bail on all three checks. The caller must add an
96
- `OVERRIDE` stamp for the review-stamp check and a `verify=PREDATES` (the
144
+ a different flow, will not carry the stamp, the Deviations block, the
145
+ `verify=` field, or the binding keys — they still bail on all four checks. The
146
+ caller must add an `OVERRIDE` stamp for the review-stamp check (which also
147
+ covers the binding check, per the note above) and a `verify=PREDATES` (the
97
148
  legacy case — not `verify=OVERRIDE`, which is reserved for an active per-cycle
98
149
  waiver) for the verify check, and must author the Deviations block (there is
99
150
  no override for its absence), to proceed. Do not silently fall through when
100
- any of the three is absent.
151
+ any of the four is absent.
101
152
 
102
153
  ## When to Use
103
154
 
@@ -901,11 +952,13 @@ prompt file:
901
952
  <!-- THRUM-GATE: stage=prompt next=dispatch -->
902
953
  ```
903
954
 
904
- 2. **After the post-setup prompt review terminates `Ready:Yes`**, append the
905
- verdict stamp:
955
+ 2. **After the post-setup prompt review terminates `Ready:Yes`**, compute
956
+ `prompt_obj`/`plan_lock` per `_stamp-protocol.md` § "Review-object binding"
957
+ (strip-then-hash the prompt against itself for `prompt_obj`; plain-hash the
958
+ LOCKED plan for `plan_lock`), then append the verdict stamp:
906
959
 
907
960
  ```text
908
- <!-- THRUM-REVIEW: stage=prompt verdict=Ready:Yes cycle=<N> date=<YYYY-MM-DD> -->
961
+ <!-- THRUM-REVIEW: stage=prompt verdict=Ready:Yes cycle=<N> date=<YYYY-MM-DD> prompt_obj=<blob> plan_lock=<blob> -->
909
962
  ```
910
963
 
911
964
  This review happens **within the same project-setup session**: after Step 4
@@ -916,10 +969,54 @@ prompt file:
916
969
 
917
970
  Use the exact canonical, case-sensitive form (`Ready:Yes` / `OVERRIDE`; keep
918
971
  keys in the fixed order `stage=`, `verdict=`, `cycle=`, `date=` so a literal
919
- `grep -F` matches). At dispatch, the coordinator greps the prompt for this
920
- stamp: present → the pre-dispatch review is already satisfied (skip re-review);
921
- absent → fall through to the normal pre-dispatch dual review (the stamp is a
922
- shortcut, not a hard dispatch requirement).
972
+ `grep -F` matches).
973
+
974
+ **This stamp is a hard completion requirement, not an optional shortcut.**
975
+ project-setup CANNOT report completion or dispatch-readiness for this session
976
+ until the generated prompt carries a `stage=prompt verdict=Ready:Yes` (or
977
+ `OVERRIDE`) THRUM-REVIEW line with valid `prompt_obj`/`plan_lock` bindings.
978
+ Run the same binding check used at Phase 0 (§ "Fourth check" above), against
979
+ the prompt instead of the plan:
980
+
981
+ ```bash
982
+ # PROMPT_FILE = the generated prompt; PLAN_FILE = the LOCKED plan it was reviewed against
983
+ LINE=$(grep '^<!-- THRUM-REVIEW: stage=prompt ' "$PROMPT_FILE" | tail -1)
984
+ [ -n "$LINE" ] || {
985
+ echo "prompt carries no stage=prompt review stamp — cannot report dispatch-ready" >&2
986
+ exit 1
987
+ }
988
+
989
+ # An OVERRIDE on this line already satisfies the check (same escape as
990
+ # Phase 0's fourth check) — short-circuit BEFORE the missing-keys exit below,
991
+ # or a legitimate OVERRIDE stamp that omits prompt_obj/plan_lock is wrongly
992
+ # rejected.
993
+ if ! grep -qE 'verdict=OVERRIDE([[:space:]]|-->|$)' <<<"$LINE"; then
994
+ STAMPED_OBJ=$(sed -n 's/.*prompt_obj=\([0-9a-f]\{40,64\}\).*/\1/p' <<<"$LINE")
995
+ STAMPED_LOCK=$(sed -n 's/.*plan_lock=\([0-9a-f]\{40,64\}\).*/\1/p' <<<"$LINE")
996
+ [ -n "$STAMPED_OBJ" ] && [ -n "$STAMPED_LOCK" ] || {
997
+ echo "review stamp missing prompt_obj/plan_lock binding keys" >&2; exit 1; }
998
+
999
+ CURRENT_OBJ=$(grep -v '^<!-- THRUM-REVIEW:' "$PROMPT_FILE" | git hash-object --no-filters --stdin)
1000
+ CURRENT_LOCK=$(git hash-object --no-filters "$PLAN_FILE")
1001
+
1002
+ [ "$STAMPED_OBJ" = "$CURRENT_OBJ" ] \
1003
+ || { echo "review stamp does not bind the current prompt object" >&2; exit 1; }
1004
+ [ "$STAMPED_LOCK" = "$CURRENT_LOCK" ] \
1005
+ || { echo "review stamp bound to a superseded plan LOCK" >&2; exit 1; }
1006
+ fi
1007
+ ```
1008
+
1009
+ An OVERRIDE on the matched line already satisfies this check (same escape as
1010
+ Phase 0's fourth check) — the snippet above short-circuits before re-deriving
1011
+ or comparing in that case.
1012
+
1013
+ At dispatch, the coordinator greps the prompt for this stamp. On a
1014
+ project-setup-produced prompt it is ALWAYS present — project-setup will not
1015
+ have reported completion without it — so its absence on such a prompt is an
1016
+ ANOMALY requiring the full pre-dispatch review, never a sanctioned shortcut
1017
+ for skipping it. (Pre-feature prompts and prompts from non-project-setup
1018
+ flows are a separate, narrower carve-out — see
1019
+ `coordinator-dispatching-work`.)
923
1020
 
924
1021
  ## Common Mistakes
925
1022
 
@@ -112,7 +112,9 @@ Give each sub-agent:
112
112
  `merge-base`, `status` ONLY — never `checkout`/`reset`/`restore`/`stash`/
113
113
  `clean`/`rebase`/`commit`/`push`, in any directory; never enter another
114
114
  worktree.
115
- - pinned `model:` + `effort:` (sonnet, low for mechanical lookups).
115
+ - pinned `model:` (sonnet; intended tier is low for mechanical lookups — the
116
+ Agent tool takes no `effort`, so carry the tier via the agent definition or
117
+ the tmux launch flags).
116
118
 
117
119
  ## Step 5 — Add missing committed work
118
120
 
@@ -0,0 +1,39 @@
1
+ {
2
+ "version": 1,
3
+ "_supersedes": "the earlier watcher-settings allowlist spec (single-watcher restart-flow scope)",
4
+ "command_patterns": [
5
+ "thrum"
6
+ ],
7
+ "read_paths": [
8
+ ".claude/skills",
9
+ ".claude/commands",
10
+ ".codex/skills",
11
+ ".opencode/skills",
12
+ ".thrum"
13
+ ],
14
+ "global_read_paths": {
15
+ "claude": [
16
+ "~/.claude/plugins/cache/thrum",
17
+ "~/.local/bin"
18
+ ],
19
+ "codex": [
20
+ "~/.codex/skills",
21
+ "~/.codex/plugins/cache/thrum-marketplace/thrum",
22
+ "~/.local/bin"
23
+ ],
24
+ "opencode": [
25
+ "~/.local/bin"
26
+ ]
27
+ },
28
+ "owner_authorized_exceptions": {
29
+ "claude": [
30
+ "/private/tmp"
31
+ ],
32
+ "codex": [
33
+ "/private/tmp"
34
+ ],
35
+ "opencode": [
36
+ "/private/tmp"
37
+ ]
38
+ }
39
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,44 @@
1
+ // Drift guard (allowlist round 2, finding 9): the canonical allowlist
2
+ // (internal/permissions/thrum_allowlist.json, Go-owned) is copied verbatim
3
+ // into this plugin's bundled asset (opencode-plugin/assets/thrum_allowlist.
4
+ // json) by scripts/sync-skills.sh's sync_opencode step, since TypeScript
5
+ // cannot import a Go package. This test fails LOUDLY the moment the two
6
+ // diverge — a hand-edit of either copy, or a Go-side change that never got
7
+ // synced.
8
+ //
9
+ // Deliberately compares RAW BYTES, not parsed-then-reserialized JSON: a
10
+ // round-trip through JSON.parse/JSON.stringify would normalize whitespace,
11
+ // key order, and formatting, and could mask a real drift (e.g. a
12
+ // re-ordered or re-indented — but content-different — copy would compare
13
+ // "equal" after reserialization while still being a byte-for-byte
14
+ // divergence from the Go source of truth).
15
+ import { test } from "node:test";
16
+ import assert from "node:assert/strict";
17
+ import fs from "fs";
18
+ import path from "path";
19
+ // This test file compiles to dist/allowlist-sync.test.js (flat src ->
20
+ // flat dist, see tsconfig rootDir/outDir), so at runtime
21
+ // path.dirname(import.meta.url) is opencode-plugin/dist. One level up is
22
+ // opencode-plugin/ (matches the existing ALLOWLIST_ASSET_PATH convention in
23
+ // installer.permissions.test.ts); two levels up is the repo root, from
24
+ // which internal/permissions/ is reachable.
25
+ const HERE = path.dirname(new URL(import.meta.url).pathname);
26
+ const PLUGIN_ROOT = path.resolve(HERE, "..");
27
+ const REPO_ROOT = path.resolve(PLUGIN_ROOT, "..");
28
+ const OPENCODE_COPY = path.join(PLUGIN_ROOT, "assets", "thrum_allowlist.json");
29
+ const CANONICAL_SOURCE = path.join(REPO_ROOT, "internal", "permissions", "thrum_allowlist.json");
30
+ test("opencode-plugin/assets/thrum_allowlist.json is byte-identical to internal/permissions/thrum_allowlist.json", (t) => {
31
+ // The canonical source only exists in the monorepo (github.com/leonletto/thrum);
32
+ // the public standalone opencode-thrum-pro repo this test also ships in does not
33
+ // carry internal/permissions/, so skip gracefully there instead of failing —
34
+ // mirrors the binary-absent skip idiom in permission-fixture.integration.test.ts.
35
+ if (!fs.existsSync(CANONICAL_SOURCE)) {
36
+ t.skip(`canonical source not found at ${CANONICAL_SOURCE} — expected outside the monorepo (e.g. public opencode-thrum-pro), skipping drift check`);
37
+ return;
38
+ }
39
+ assert.ok(fs.existsSync(OPENCODE_COPY), `synced copy not found at ${OPENCODE_COPY}`);
40
+ const canonical = fs.readFileSync(CANONICAL_SOURCE);
41
+ const copy = fs.readFileSync(OPENCODE_COPY);
42
+ assert.ok(canonical.equals(copy), "opencode-plugin/assets/thrum_allowlist.json has drifted from internal/permissions/thrum_allowlist.json — " +
43
+ "re-run scripts/sync-skills.sh (sync_opencode step) to resync, never hand-edit either copy independently");
44
+ });
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { installAssets } from "./installer.js";
1
+ import { installAssets, installPermissions } from "./installer.js";
2
2
  import { shellEnvHook } from "./hooks/shell-env.js";
3
3
  import { blockSyncCdHook } from "./hooks/block-sync-cd.js";
4
4
  const TOOL_MAPPING = `
@@ -13,6 +13,29 @@ When Thrum skills reference Claude Code tool names, use these OpenCode equivalen
13
13
  `.trim();
14
14
  const server = async (ctx) => {
15
15
  await installAssets(ctx);
16
+ // Runs unconditionally (not version-gated like installAssets above): it's
17
+ // cheap and idempotent, and gating it on the skills-version marker would
18
+ // skip re-merging the allowlist for users who already had the plugin
19
+ // installed before this permission block existed (allowlist).
20
+ //
21
+ // installPermissions() throws when the user's existing global
22
+ // opencode.json is present but malformed (unreadable or invalid JSON) —
23
+ // deliberately, so we never silently overwrite it (allowlist landing-
24
+ // blocker). Catch and log here rather than letting it become an unhandled
25
+ // rejection that would abort plugin bootstrap entirely; every other hook
26
+ // this plugin provides should still register.
27
+ try {
28
+ await installPermissions();
29
+ }
30
+ catch (err) {
31
+ await ctx.client.app.log({
32
+ body: {
33
+ service: "opencode-thrum",
34
+ level: "error",
35
+ message: `failed to install thrum permission allowlist (leaving your opencode.json untouched): ${err.message}`,
36
+ },
37
+ });
38
+ }
16
39
  return {
17
40
  "shell.env": shellEnvHook(ctx),
18
41
  "tool.execute.before": blockSyncCdHook(),
@@ -1,2 +1,31 @@
1
1
  import type { PluginInput } from "@opencode-ai/plugin";
2
+ export declare function getConfigDir(): string;
2
3
  export declare function installAssets(ctx: PluginInput): Promise<void>;
4
+ interface AllowlistSource {
5
+ version: number;
6
+ command_patterns: string[];
7
+ read_paths: string[];
8
+ owner_authorized_exceptions?: Record<string, string[]>;
9
+ global_read_paths?: Record<string, string[]>;
10
+ }
11
+ type PermissionAction = "ask" | "allow" | "deny";
12
+ type PermissionRuleMap = Record<string, PermissionAction>;
13
+ interface OpenCodeConfig {
14
+ permission?: {
15
+ bash?: PermissionRuleMap | PermissionAction;
16
+ read?: PermissionRuleMap | PermissionAction;
17
+ [key: string]: unknown;
18
+ };
19
+ [key: string]: unknown;
20
+ }
21
+ export declare function bashExactPatternFor(commandPattern: string): string;
22
+ export declare function bashPatternFor(commandPattern: string): string;
23
+ export declare function readPatternFor(readPath: string): string;
24
+ export declare function resolveHomePath(p: string, homeDir?: string): string;
25
+ export declare function readFilePatternFor(filePath: string): string;
26
+ export declare function loadAllowlist(assetPath?: string): AllowlistSource;
27
+ export declare function mergePermissions(config: OpenCodeConfig, allowlist: AllowlistSource, extraReadPatterns?: string[], homeDir?: string): OpenCodeConfig;
28
+ export declare function writeConfig(configPath: string, config: OpenCodeConfig): void;
29
+ export declare function opencodeGlobalReadPaths(configDir: string): string[];
30
+ export declare function installPermissions(configDir?: string, homeDir?: string): Promise<void>;
31
+ export {};