@useshifu/coding-harness 0.2.2 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,13 +7,13 @@ npx @useshifu/coding-harness install --harness codex
7
7
  npx @useshifu/coding-harness connect --harness codex
8
8
  ```
9
9
 
10
- `connect` and `sync` each ask for confirmation. The Codex connector has no lifecycle hooks and never uploads in the background. Claude Code and OpenCode time-window discovery are not available yet.
10
+ `connect` and `sync` each ask for confirmation. The connector has no lifecycle hooks and never uploads in the background. Codex and OpenCode support local time-window discovery and review; Claude Code does not yet.
11
11
 
12
- When asked to sync, Codex discovers unsynced user turns from the last 24 hours by default. A user can choose 48 or 72 hours instead. Discovery reports only opaque session references, timestamps, and unsynced turn ranges; it does not send transcript content. `blockedSessions` lists recently changed sessions whose next unsynced turn is older than the chosen window. Those older contiguous backlogs are not silently included; the user can explicitly select a session and review it with `--all`.
12
+ When asked to sync, Codex and OpenCode discover unsynced user turns from the last 24 hours by default. A user can choose 48 or 72 hours instead. Discovery reports only opaque session references, timestamps, and unsynced turn ranges; it does not send transcript content. `blockedSessions` lists recently changed sessions whose next unsynced turn is older than the chosen window. Those older contiguous backlogs are not silently included; the user can explicitly select a session and review it with `--all`.
13
13
 
14
14
  ```sh
15
- npx @useshifu/coding-harness sessions --harness codex --hours 24
16
- npx @useshifu/coding-harness review --harness codex --session-ref opaque-session-id
15
+ npx @useshifu/coding-harness sessions --harness <codex|opencode> --hours 24
16
+ npx @useshifu/coding-harness review --harness <codex|opencode> --session-ref opaque-session-id
17
17
  ```
18
18
 
19
19
  `review` reads the next contiguous segment of at most 12 unsynced turns locally. Pass the selected `--hours` value to `review` as well; `--all` is reserved for a specifically selected session's older backlog. It shows assistant final notes, candidate work items, and a draft that leaves required sync and work-item titles blank for the reviewer. Candidates are suggestions, not proof or guaranteed redaction. Candidate scope is left unset until a reviewer chooses the smallest supported level. If a segment has more than 64 candidates, rerun `sessions` and `review` with `--turns 1` to narrow it before syncing. Review each work title, detail, and area, prepare any verification, and preserve an item's `itemRef` when a later segment revises the same work. User-message records are omitted, but assistant notes may quote sensitive material. Nothing is uploaded by this command. Repeat discovery after each accepted segment to continue the delta.
@@ -26,4 +26,8 @@ printf '%s' '{"harness":"codex","sessionRef":"opaque-session-id","fromTurn":1,"t
26
26
 
27
27
  The server stores each approved sync title and summary, plus every redacted work-item title and statement as a separate claim with its scope, optional area and item reference, cited verification, and source receipt. Uncited verification wording, standalone decisions, raw assistant notes, and transcripts are not retained as claim text. `area`, `itemRef`, and `verificationRefs` are optional; citing a verification item applies only to that work item and is displayed separately from its work statement. A decision becomes a claim only when it is included as an `evidence` item with `kind: "decision"`. At least one reviewed work item is required to advance a checkpoint. The complete cross-component contract is in [coding-session-sync-contract.md](../../docs/coding-session-sync-contract.md).
28
28
 
29
+ ## Review quality
30
+
31
+ Write each sync as a concise technical record that will remain useful after the raw session is unavailable. Separate architecture, implementation, security controls, rollout, and user-facing work when they have distinct boundaries. Describe the mechanism, boundary, and material constraint for each item; state known limits plainly. Put material architecture, security, and rollout choices in `decision` evidence if they should become claims. Keep tests and operational checks in `verification`, citing them from the work item they support. Do not claim impact, ownership, or outcomes without direct support.
32
+
29
33
  The default API URL is `https://api.useshifu.com`. Override it during local or self-hosted use with `--api-url` on `connect`, or `SHIFU_API_URL`.
@@ -3,6 +3,7 @@
3
3
  const fs = require("node:fs");
4
4
  const os = require("node:os");
5
5
  const path = require("node:path");
6
+ const { execFileSync } = require("node:child_process");
6
7
  const readline = require("node:readline/promises");
7
8
  const tty = require("node:tty");
8
9
 
@@ -15,6 +16,8 @@ const HARNESS_NAMES = {
15
16
  const TOKEN_PATTERN = /^cs_sk_[a-f0-9]{16}_[a-f0-9]{64}$/;
16
17
  const SENSITIVE_PATTERNS = ["-----begin", "authorization", "sk-", "sk_", "ghp_", "gho_", "ghu_", "ghs_", "ghr_", "xox", "github_pat_", "glpat-", "ignore previous instructions", "ignore all previous instructions", "system prompt", "@", "://", "`", "/"];
17
18
  const PHONE_NUMBER_PATTERN = /(?:\+?\d[\d .()-]{6,}\d)/i;
19
+ const OPAQUE_SESSION_REF_PATTERN = /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|(?:ses|thread)_[a-z0-9_-]+)$/i;
20
+ const UNSAFE_SESSION_REF_PATTERNS = ["api_key", "authorization", "password", "@", "://", "`", "/"];
18
21
  const UNSAFE_CODE_PATTERN = /\b(?:func|class|interface|struct|package|import|select|insert|update|delete|create\s+table)\s+[a-z_][a-z0-9_]*\s*(?:\(|\{|=|$)/i;
19
22
  const CREDENTIAL_ASSIGNMENT_PATTERN = /\b(?:token|secret|password|api[_-]?key)\s*[:=]\s*\S+/i;
20
23
  const CLOUD_CREDENTIAL_PATTERN = /\bAKIA[0-9A-Z]{16}\b|AIza[0-9A-Za-z_-]{20,}/;
@@ -50,6 +53,73 @@ function codexSessionsRoot() {
50
53
  return process.env.SHIFU_CODEX_SESSIONS_ROOT || path.join(os.homedir(), ".codex", "sessions");
51
54
  }
52
55
 
56
+ function opencodeQuery(query) {
57
+ if (process.env.SHIFU_OPENCODE_DB && !process.env.SHIFU_OPENCODE_BIN) {
58
+ try {
59
+ const sqliteOutput = execFileSync("sqlite3", ["-json", process.env.SHIFU_OPENCODE_DB, query], {
60
+ encoding: "utf8",
61
+ stdio: ["ignore", "pipe", "pipe"],
62
+ maxBuffer: 64 * 1024 * 1024,
63
+ });
64
+ return JSON.parse(sqliteOutput);
65
+ } catch {
66
+ throw new Error("Could not read OpenCode session metadata. Ensure the opencode command is installed and its local database is available.");
67
+ }
68
+ }
69
+ const temporary = path.join(os.tmpdir(), `shifu-opencode-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.json`);
70
+ let fd;
71
+ try {
72
+ fd = fs.openSync(temporary, "w");
73
+ execFileSync(process.env.SHIFU_OPENCODE_BIN || "opencode", ["db", query, "--format", "json"], {
74
+ stdio: ["ignore", fd, "pipe"],
75
+ maxBuffer: 64 * 1024 * 1024,
76
+ });
77
+ fs.closeSync(fd);
78
+ fd = undefined;
79
+ return JSON.parse(fs.readFileSync(temporary, "utf8"));
80
+ } catch {
81
+ if (fd !== undefined) {
82
+ try { fs.closeSync(fd); } catch {}
83
+ fd = undefined;
84
+ }
85
+ const dbPath = process.env.SHIFU_OPENCODE_DB || path.join(process.env.XDG_DATA_HOME || path.join(os.homedir(), ".local", "share"), "opencode", "opencode.db");
86
+ if (fs.existsSync(dbPath)) {
87
+ try {
88
+ const sqliteOutput = execFileSync("sqlite3", ["-json", dbPath, query], {
89
+ encoding: "utf8",
90
+ stdio: ["ignore", "pipe", "pipe"],
91
+ maxBuffer: 64 * 1024 * 1024,
92
+ });
93
+ return JSON.parse(sqliteOutput);
94
+ } catch {}
95
+ }
96
+ throw new Error("Could not read OpenCode session metadata. Ensure the opencode command is installed and its local database is available.");
97
+ } finally {
98
+ if (fd !== undefined) {
99
+ try { fs.closeSync(fd); } catch {}
100
+ }
101
+ try { fs.unlinkSync(temporary); } catch {}
102
+ }
103
+ }
104
+
105
+ function opencodeSessions(rows) {
106
+ const sessions = new Map();
107
+ for (const row of rows) {
108
+ if (typeof row.sessionRef !== "string" || !Number.isFinite(row.startedAt) || !Number.isFinite(row.updatedAt) || !Number.isFinite(row.turnAt)) continue;
109
+ const session = sessions.get(row.sessionRef) || { sessionRef: row.sessionRef, startedAt: new Date(row.startedAt).toISOString(), updatedAt: new Date(row.updatedAt).toISOString(), turnCount: 0, turnTimes: [] };
110
+ session.turnCount += 1;
111
+ session.turnTimes.push(new Date(row.turnAt).toISOString());
112
+ sessions.set(row.sessionRef, session);
113
+ }
114
+ return [...sessions.values()];
115
+ }
116
+
117
+ function localSessions(harness) {
118
+ if (harness === "codex") return sessionFiles(codexSessionsRoot()).map(codexSession).filter(Boolean);
119
+ if (harness === "opencode") return opencodeSessions(opencodeQuery("SELECT s.id AS sessionRef, s.time_created AS startedAt, s.time_updated AS updatedAt, m.time_created AS turnAt FROM session s JOIN message m ON m.session_id = s.id WHERE json_extract(m.data, '$.role') = 'user' ORDER BY s.time_updated, m.time_created"));
120
+ throw new Error("Session discovery is currently available for Codex and OpenCode only.");
121
+ }
122
+
53
123
  function requireHarness(value) {
54
124
  if (!Object.hasOwn(HARNESS_NAMES, value)) throw new Error("Choose --harness codex, claude_code, or opencode.");
55
125
  return value;
@@ -203,6 +273,46 @@ function codexReview(file, fromTurn, toTurn) {
203
273
  return { sessionRef, turnCount: turn, notes };
204
274
  }
205
275
 
276
+ function opencodeReview(sessionRef, fromTurn, toTurn) {
277
+ const rows = opencodeQuery(`SELECT m.id AS messageId, m.time_created AS createdAt, json_extract(m.data, '$.role') AS role, json_extract(m.data, '$.finish') AS finish, p.data AS part FROM message m LEFT JOIN part p ON p.message_id = m.id WHERE m.session_id = ${JSON.stringify(sessionRef)} ORDER BY m.time_created, p.id`);
278
+ const messages = new Map();
279
+ for (const row of rows) {
280
+ const message = messages.get(row.messageId) || { role: row.role, finish: row.finish, parts: [] };
281
+ if (typeof row.part === "string") {
282
+ try {
283
+ const part = JSON.parse(row.part);
284
+ if (part.type === "text" && typeof part.text === "string") message.parts.push(part.text);
285
+ } catch {}
286
+ }
287
+ messages.set(row.messageId, message);
288
+ }
289
+ let turn = 0;
290
+ const turnMessages = new Map();
291
+ for (const message of messages.values()) {
292
+ if (message.role === "user") {
293
+ turn += 1;
294
+ turnMessages.set(turn, []);
295
+ continue;
296
+ }
297
+ if (message.role === "assistant" && turn > 0) {
298
+ turnMessages.get(turn).push(message);
299
+ }
300
+ }
301
+ const notes = [];
302
+ for (const [t, msgs] of turnMessages.entries()) {
303
+ if (t < fromTurn || t > toTurn) continue;
304
+ let candidates = msgs.filter((m) => m.finish === "stop" || (m.finish && m.finish !== "tool-calls"));
305
+ if (candidates.length === 0) {
306
+ candidates = msgs.filter((m) => m.parts.length > 0).slice(-1);
307
+ }
308
+ for (const m of candidates) {
309
+ const note = m.parts.join("\n").trim();
310
+ if (note) notes.push({ turn: t, note });
311
+ }
312
+ }
313
+ return { sessionRef, turnCount: turn, notes };
314
+ }
315
+
206
316
  function reviewCandidates(notes) {
207
317
  const candidates = [];
208
318
  const seen = new Set();
@@ -224,15 +334,12 @@ function reviewCandidates(notes) {
224
334
  }
225
335
 
226
336
  function unsyncedCodexSessions(harness, hours, turns = MAX_TURNS_PER_SEGMENT, selectedRef, includeOlder = false) {
227
- if (harness !== "codex") throw new Error("Session discovery is currently available for Codex only.");
228
337
  if (![24, 48, 72].includes(hours)) throw new Error("Choose --hours 24, 48, or 72.");
229
338
  if (!Number.isInteger(turns) || turns < 1 || turns > MAX_TURNS_PER_SEGMENT) throw new Error("Choose --turns between 1 and 12.");
230
339
  const since = Date.now() - hours * 3_600_000;
231
340
  const state = readState(harness);
232
341
  const sessions = new Map();
233
- for (const session of sessionFiles(codexSessionsRoot())
234
- .map(codexSession)
235
- .filter(Boolean)
342
+ for (const session of localSessions(harness)
236
343
  .filter((session) => selectedRef ? session.sessionRef === selectedRef : Date.parse(session.updatedAt) >= since)) {
237
344
  const existing = sessions.get(session.sessionRef);
238
345
  sessions.set(session.sessionRef, existing ? {
@@ -353,16 +460,31 @@ function removeCodexHooks() {
353
460
  function installDestination(harness) {
354
461
  if (harness === "codex") return path.join(os.homedir(), ".codex", "skills", "shifu-sync");
355
462
  if (harness === "claude_code") return path.join(os.homedir(), ".claude", "skills", "shifu-sync");
356
- return path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"), "opencode", "commands", "shifu-sync.md");
463
+ return path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"), "opencode", "skills", "shifu-sync");
357
464
  }
358
465
 
359
466
  function install(harness) {
360
467
  copyRunner();
361
- const destination = installDestination(harness);
362
468
  if (harness === "opencode") {
363
- fs.mkdirSync(path.dirname(destination), { recursive: true, mode: 0o700 });
364
- fs.copyFileSync(path.join(__dirname, "..", "commands", "shifu-sync.md"), destination);
469
+ const configDir = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
470
+ const skillDest = path.join(configDir, "opencode", "skills", "shifu-sync");
471
+ fs.mkdirSync(skillDest, { recursive: true, mode: 0o700 });
472
+ const skillContent = fs.readFileSync(path.join(__dirname, "..", "skills", "shifu-sync", "SKILL.md"), "utf8")
473
+ .replaceAll('"codex"', '"opencode"')
474
+ .replaceAll("--harness codex", "--harness opencode")
475
+ .replaceAll("Codex", "OpenCode")
476
+ .replaceAll("npx @useshifu/coding-harness", `"${process.execPath}" "${installedRunnerPath()}"`);
477
+ fs.writeFileSync(path.join(skillDest, "SKILL.md"), skillContent, { mode: 0o600 });
478
+
479
+ const commandContent = fs.readFileSync(path.join(__dirname, "..", "commands", "shifu-sync.md"), "utf8")
480
+ .replaceAll("npx @useshifu/coding-harness", `"${process.execPath}" "${installedRunnerPath()}"`);
481
+ for (const folder of ["command", "commands"]) {
482
+ const commandDir = path.join(configDir, "opencode", folder);
483
+ fs.mkdirSync(commandDir, { recursive: true, mode: 0o700 });
484
+ fs.writeFileSync(path.join(commandDir, "shifu-sync.md"), commandContent, { mode: 0o600 });
485
+ }
365
486
  } else {
487
+ const destination = installDestination(harness);
366
488
  copyDirectory(path.join(__dirname, "..", "skills", "shifu-sync"), destination);
367
489
  const skill = path.join(destination, "SKILL.md");
368
490
  const content = fs.readFileSync(skill, "utf8")
@@ -393,10 +515,17 @@ function textIsSafe(value, maximum) {
393
515
  return typeof value === "string" && value.trim().length > 0 && value.length <= maximum && !/[\x00-\x1f\x7f]/.test(value) && !PHONE_NUMBER_PATTERN.test(value) && !UNSAFE_CODE_PATTERN.test(value) && !CREDENTIAL_ASSIGNMENT_PATTERN.test(value) && !CLOUD_CREDENTIAL_PATTERN.test(value) && !SENSITIVE_PATTERNS.some((pattern) => value.toLowerCase().includes(pattern));
394
516
  }
395
517
 
518
+ function sessionRefIsSafe(value) {
519
+ if (typeof value !== "string") return false;
520
+ const trimmed = value.trim();
521
+ if (UNSAFE_SESSION_REF_PATTERNS.some((pattern) => trimmed.toLowerCase().includes(pattern))) return false;
522
+ return trimmed.length >= 8 && trimmed.length <= 200 && (OPAQUE_SESSION_REF_PATTERN.test(trimmed) || textIsSafe(trimmed, 200));
523
+ }
524
+
396
525
  function validateSync(input, harness) {
397
526
  if (!input || Object.keys(input).some((field) => !SYNC_FIELDS.has(field))) throw new Error("Remove unsupported fields from the reviewed sync payload.");
398
- if (!input || input.harness !== harness || typeof input.sessionRef !== "string" || !textIsSafe(input.sessionRef, 200) || input.sessionRef.length < 8) throw new Error("Add the current opaque sessionRef before syncing.");
399
- if (!Number.isInteger(input.fromTurn) || !Number.isInteger(input.toTurn) || input.fromTurn < 1 || input.toTurn < input.fromTurn || input.toTurn - input.fromTurn >= MAX_TURNS_PER_SEGMENT) throw new Error("Use a valid incremental range of at most 12 turns.");
527
+ if (!input || input.harness !== harness || !sessionRefIsSafe(input.sessionRef)) throw new Error("Add the current opaque sessionRef before syncing.");
528
+ if (!Number.isInteger(input.fromTurn) || !Number.isInteger(input.toTurn) || input.fromTurn < 1 || input.toTurn < input.fromTurn || input.toTurn > 1_000_000 || input.toTurn - input.fromTurn >= MAX_TURNS_PER_SEGMENT) throw new Error("Use a valid incremental range of at most 12 turns.");
400
529
  if (input.redactionVersion === 3 && !textIsSafe(input.title, MAX_TITLE_LENGTH)) throw new Error("Add a concise, redacted title for this reviewed sync.");
401
530
  if (!textIsSafe(input.summary, 1200)) throw new Error("The summary is empty, too long, or contains sensitive content. Redact it before syncing.");
402
531
  if (!Array.isArray(input.evidence) || input.evidence.length < 1 || input.evidence.length > MAX_EVIDENCE_ITEMS || !input.evidence.every((item) => item && EVIDENCE_KINDS.has(item.kind) && SCOPE_LEVELS.has(item.scope) && (input.redactionVersion !== 3 || textIsSafe(item.title, MAX_TITLE_LENGTH)) && textIsSafe(item.statement, MAX_WORK_DETAIL_LENGTH))) {
@@ -414,6 +543,12 @@ function validateSync(input, harness) {
414
543
  item.verificationRefs.every((ref) => Number.isInteger(ref) && ref >= 0 && ref < input.verification.length))))) {
415
544
  throw new Error("Each work item must use a supported area, safe itemRef, and distinct indexes into verification.");
416
545
  }
546
+ const identities = new Set();
547
+ for (const item of input.evidence) {
548
+ const identity = item.itemRef || `${item.kind}:${item.scope}:${item.statement.trim()}`;
549
+ if (identities.has(identity)) throw new Error("Each work item must have a distinct identity.");
550
+ identities.add(identity);
551
+ }
417
552
  if (input.redactionVersion !== 2 && input.redactionVersion !== 3) throw new Error("Use redactionVersion 3 for new reviewed syncs.");
418
553
  }
419
554
 
@@ -479,51 +614,48 @@ function sessions(harness) {
479
614
  }
480
615
 
481
616
  function review(harness) {
482
- if (harness !== "codex") throw new Error("Session review is currently available for Codex only.");
483
617
  readConfig(harness);
484
618
  const sessionRef = option("--session-ref");
485
- if (typeof sessionRef !== "string" || sessionRef.length < 8) throw new Error("Provide an opaque Codex --session-ref from the sessions command.");
619
+ if (typeof sessionRef !== "string" || sessionRef.length < 8) throw new Error(`Provide an opaque ${HARNESS_NAMES[harness]} --session-ref from the sessions command.`);
486
620
  const state = readState(harness);
487
621
  const fromTurn = (state.sessions[sessionRef]?.lastSyncedTurn || 0) + 1;
488
- const matchingFiles = sessionFiles(codexSessionsRoot())
489
- .map((file) => ({ file, session: codexSession(file) }))
490
- .filter(({ session }) => session?.sessionRef === sessionRef)
491
- .sort((left, right) => right.session.turnCount - left.session.turnCount || right.session.updatedAt.localeCompare(left.session.updatedAt));
492
- const selected = matchingFiles[0];
493
- if (!selected) throw new Error("No local Codex session matches this reference.");
622
+ const selected = localSessions(harness).filter((session) => session.sessionRef === sessionRef)
623
+ .sort((left, right) => right.turnCount - left.turnCount || right.updatedAt.localeCompare(left.updatedAt))[0];
624
+ if (!selected) throw new Error(`No local ${HARNESS_NAMES[harness]} session matches this reference.`);
494
625
  const hours = numberOption("--hours", 24);
495
626
  if (![24, 48, 72].includes(hours)) throw new Error("Choose --hours 24, 48, or 72.");
496
- if (!hasFlag("--all") && !(Date.parse(selected.session.turnTimes[fromTurn - 1]) >= Date.now() - hours * 3_600_000)) {
627
+ if (!hasFlag("--all") && !(Date.parse(selected.turnTimes[fromTurn - 1]) >= Date.now() - hours * 3_600_000)) {
497
628
  throw new Error("This session's next unsynced turn predates the selected window. Ask for this specific session and use --all to review its backlog.");
498
629
  }
499
630
  const turns = numberOption("--turns", MAX_TURNS_PER_SEGMENT);
500
631
  if (!Number.isInteger(turns) || turns < 1 || turns > MAX_TURNS_PER_SEGMENT) throw new Error("Choose --turns between 1 and 12.");
501
- const toTurn = Math.min(selected.session.turnCount, fromTurn + turns - 1);
502
- if (fromTurn > toTurn) throw new Error("This Codex session has no unsynced turns to review.");
503
- const { notes } = codexReview(selected.file, fromTurn, toTurn);
632
+ const toTurn = Math.min(selected.turnCount, fromTurn + turns - 1);
633
+ if (fromTurn > toTurn) throw new Error(`This ${HARNESS_NAMES[harness]} session has no unsynced turns to review.`);
634
+ const { notes } = harness === "codex" ? codexReview(sessionFiles(codexSessionsRoot()).map((file) => ({ file, session: codexSession(file) })).filter(({ session }) => session?.sessionRef === sessionRef).sort((left, right) => right.session.turnCount - left.session.turnCount)[0].file, fromTurn, toTurn) : opencodeReview(sessionRef, fromTurn, toTurn);
504
635
  const candidates = reviewCandidates(notes);
505
636
  console.log(JSON.stringify({
506
637
  sessionRef,
507
638
  fromTurn,
508
639
  toTurn,
509
- oldestUnsyncedAt: selected.session.turnTimes[fromTurn - 1] || null,
640
+ oldestUnsyncedAt: selected.turnTimes[fromTurn - 1] || null,
510
641
  source: "Local assistant final notes for unsynced user turns. Candidates are suggestions, not verified or guaranteed redacted; inspect them before constructing a Shifu payload.",
511
642
  notes,
512
643
  candidates,
513
644
  draft: { title: null, summary: null, evidence: candidates.map((candidate) => ({ ...candidate, title: null })) },
514
645
  candidateOverflow: candidates.length > MAX_EVIDENCE_ITEMS,
515
- remainingTurns: selected.session.turnCount - toTurn,
646
+ remainingTurns: selected.turnCount - toTurn,
516
647
  }, null, 2));
517
648
  }
518
649
 
519
650
  async function main() {
520
651
  const command = process.argv[2];
652
+ const subCommand = process.argv[3];
521
653
  const harness = requireHarness(option("--harness"));
522
654
  if (command === "install") return install(harness);
523
655
  if (command === "connect") return connect(harness);
524
656
  if (command === "sync") return sync(harness);
525
657
  if (command === "status") return status(harness);
526
- if (command === "sessions") return sessions(harness);
658
+ if (command === "sessions" || command === "discover" || (command === "session" && (subCommand === "discover" || subCommand === "list" || !subCommand || subCommand.startsWith("-")))) return sessions(harness);
527
659
  if (command === "review") return review(harness);
528
660
  throw new Error("Use install, connect, status, sessions, review, or sync.");
529
661
  }
@@ -537,4 +669,4 @@ if (require.main === module) {
537
669
  });
538
670
  }
539
671
 
540
- module.exports = { codexReview, codexSession, configPath, finalAssistantNote, readSecret, removeHooks, requireSafeApiUrl, reviewCandidates, statePath, textIsSafe, unsyncedCodexSessions, validSyncReceipt, validateSync };
672
+ module.exports = { codexReview, codexSession, configPath, finalAssistantNote, install, installDestination, localSessions, opencodeQuery, opencodeReview, opencodeSessions, readSecret, removeHooks, requireSafeApiUrl, reviewCandidates, statePath, textIsSafe, unsyncedCodexSessions, validSyncReceipt, validateSync };
@@ -2,10 +2,46 @@
2
2
  description: Prepare a reviewed, redacted incremental Shifu sync and ask for confirmation.
3
3
  ---
4
4
 
5
- Use the Shifu reviewed-sync process for the current OpenCode session. Never upload automatically or include raw transcript content. First explain what the unsynced incremental segment contains and show the exact JSON payload. Ask the user to confirm. Only after confirmation, pipe the reviewed payload to:
5
+ Use the Shifu reviewed-sync process for OpenCode only after an explicit user request. Never upload automatically or include raw transcript content. Before acting, say whether you will connect or sync and ask for confirmation.
6
+
7
+ For a sync request, use the time window the user names. If none is named, state that you will use the last 24 hours. The only supported windows are 24, 48, and 72 hours.
8
+
9
+ 1. Run `npx @useshifu/coding-harness sessions --harness opencode --hours <24|48|72>`.
10
+ 2. Use every returned opaque `sessionRef`, `fromTurn`, and `toTurn`. A segment contains at most 12 user turns.
11
+ 3. Do not upload a `blockedSessions` entry automatically. Its next unsynced turn predates the selected window. Only if the user specifically chooses that session, run discovery and review with `--session-ref <sessionRef> --all`.
12
+ 4. For each selected segment, run `npx @useshifu/coding-harness review --harness opencode --session-ref <sessionRef> --hours <24|48|72>`. Review returns local assistant notes and suggested candidates only. It never returns user prompts.
13
+ 5. If review reports more than 64 candidates, rerun discovery and review with `--turns 1`. Repeat discovery after every accepted segment until the selected pending delta is exhausted.
14
+ 6. Redact the reviewed assistant notes, show the exact JSON payload, and ask the user to confirm. Only after confirmation, pipe the payload to:
6
15
 
7
16
  ```sh
8
17
  npx @useshifu/coding-harness sync --harness opencode
9
18
  ```
10
19
 
11
- The payload must contain `harness`, an opaque `sessionRef`, a contiguous `fromTurn`/`toTurn` range of at most 12 turns, a concise redacted sync `title`, a redacted `summary`, 1–64 redacted `evidence` work items (`title`, `kind`, `statement`, `scope`), up to 16 redacted `verification` items, up to 16 redacted `decisions`, and `redactionVersion: 3`. Evidence describes the work itself; verification records how it was checked. Scope must be one of `unit`, `module`, `service`, `system`, or `product` and must be reviewed rather than guessed from the candidate. Replace names, paths, URLs, credentials, commands, prompts, source code, customer details, and proprietary identifiers with neutral descriptions.
20
+ The payload must contain `harness: "opencode"`, an opaque `sessionRef`, the returned contiguous `fromTurn`/`toTurn` range, a concise redacted sync `title`, a redacted `summary`, 1–64 redacted `evidence` work items (`title`, `kind`, `statement`, `scope`), up to 16 redacted `verification` items, up to 16 redacted `decisions`, and `redactionVersion: 3`. Evidence describes the work itself; verification records how it was checked. Scope must be one of `unit`, `module`, `service`, `system`, or `product` and must be reviewed rather than guessed from the candidate. Replace names, repository identifiers, file paths, URLs, credentials, commands, prompts, source code, customer details, and proprietary identifiers with neutral descriptions. Never invent missing details or alter the returned turn range.
21
+
22
+ Write a concise but useful durable technical record. Split architecture, implementation, security controls, configuration or rollout, and user-facing behaviour into separate evidence items when they have different boundaries. Each item should state the mechanism, responsible boundary, and material constraint. Record a material architecture, security, or rollout choice as `kind: "decision"` evidence when it should persist; do not treat the standalone `decisions` list as a durable work item. State known security limits plainly. Keep tests, builds, reviews, deployment checks, and other proof in `verification`, and cite relevant verification entries from evidence with `verificationRefs`. Use the smallest true scope and do not claim impact, ownership, or outcomes not supported by the reviewed work.
23
+
24
+ Use this structure:
25
+
26
+ ```json
27
+ {
28
+ "harness": "opencode",
29
+ "sessionRef": "opaque-session-id",
30
+ "fromTurn": 1,
31
+ "toTurn": 12,
32
+ "title": "Hardened connector checkpoints",
33
+ "summary": "Implemented a narrow change and checked the relevant behaviour.",
34
+ "evidence": [{"kind": "implementation", "title": "Added checkpoint recovery", "statement": "Implemented a focused connector change.", "scope": "service"}],
35
+ "verification": ["Focused tests passed."],
36
+ "decisions": ["Kept the change within the existing connector boundary."],
37
+ "redactionVersion": 3
38
+ }
39
+ ```
40
+
41
+ Only after the user confirms the displayed payload, run:
42
+
43
+ ```sh
44
+ printf '%s' '<reviewed JSON>' | npx @useshifu/coding-harness sync --harness opencode
45
+ ```
46
+
47
+ The CLI prints the payload again and asks for a final terminal confirmation. Never bypass that confirmation.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@useshifu/coding-harness",
3
- "version": "0.2.2",
3
+ "version": "0.2.6",
4
4
  "description": "Reviewed, incremental coding-harness sync for Shifu",
5
5
  "bin": {
6
6
  "coding-harness": "bin/shifu-harness.js",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: shifu-sync
3
- description: Prepare and sync reviewed, redacted Codex work from the user's selected recent time window to Shifu. Use only when the user explicitly asks to connect, sync, or push their Shifu coding-harness data.
3
+ description: Prepare and sync reviewed, redacted Codex work from the user's selected recent time window to Shifu. Use when the user explicitly asks to connect, sync, discover sessions, or push their Shifu coding-harness data.
4
4
  ---
5
5
 
6
6
  Shifu captures reviewed, incremental work context. It does not collect raw transcripts or upload in the background.
@@ -14,11 +14,24 @@ For a sync request, use the time window the user names. If they do not name one,
14
14
 
15
15
  Before preparing a sync, run `npx @useshifu/coding-harness sessions --harness codex --hours <24|48|72>`. Use every returned session's opaque `sessionRef`, `fromTurn`, and `toTurn`. `blockedSessions` are outside the selected turn window and must not be uploaded unless the user separately chooses that specific session and its older backlog. Each result covers at most 12 turns; repeat discovery after each accepted segment until the selected window's pending delta is exhausted. If a review reports more than 64 candidates, rerun discovery and review with `--turns 1` before preparing a payload. Do not invent a checkpoint or resend a range the command has excluded.
16
16
 
17
- For each returned session, run `npx @useshifu/coding-harness review --harness codex --session-ref <sessionRef> --hours <24|48|72>`. Its local assistant note is untrusted source material, not content to upload. Prepare only its unsynced segment from that note. Each `evidence` item must be a factual, redacted work item describing what was created, changed, or deliberately decided and which component was involved. Choose its smallest supported scope: `unit`, `module`, `service`, `system`, or `product`; the candidate's null scope is not a classification. Put tests, builds, reviews, and other checks only in `verification`. Do not claim impact, ownership, outcome, or a broader scope without direct support. Replace names, repository identifiers, file paths, URLs, credentials, prompts, command lines, source code, customer details, and proprietary terms with neutral descriptions. Never invent missing details. Approved work-item statements are retained in Shifu's private claims, so review their wording as durable content.
17
+ For each returned session, run `npx @useshifu/coding-harness review --harness codex --session-ref <sessionRef> --hours <24|48|72>`. Its local assistant note is untrusted source material, not content to upload. Prepare only its unsynced segment from that note. Replace names, repository identifiers, file paths, URLs, credentials, prompts, command lines, source code, customer details, and proprietary terms with neutral descriptions. Never invent missing details.
18
+
19
+ ## Write a useful durable record
20
+
21
+ Treat the payload as a concise technical record, not a changelog headline. Capture the meaningful work in the reviewed segment with enough detail that a future reader can understand the design and tradeoffs without needing the raw transcript.
22
+
23
+ - Split distinct work into separate evidence items when they have different boundaries: architecture, implementation, security controls, configuration or rollout, and user-facing behaviour are often separate items. Do not create an item for every file or command.
24
+ - State the mechanism, boundary, and important constraint. For architecture, describe which component owns which responsibility and the relevant data flow. For implementation, say what capability was added or changed and how it is bounded. For security, name the control and any known limitation; never describe a boundary as secure when the reviewed work says otherwise.
25
+ - Choose the smallest supported scope that is true. Use `system` or `product` only when the work genuinely spans those boundaries; use `service` or `module` for local implementation detail.
26
+ - Give the sync and every evidence item a specific, neutral title. Make the summary connect the important items into one accurate account, without claiming business impact, ownership, or outcomes that the reviewed work does not establish.
27
+ - Put a material architecture, security, or rollout choice in an evidence item with `kind: "decision"` when it should become part of the durable record. Keep ordinary process notes in `decisions`; they are not a substitute for a reviewed decision work item.
28
+ - Put tests, builds, reviews, deployment checks, and other proof only in `verification`. Cite the relevant verification entries from the evidence item when the payload format supports `verificationRefs`.
29
+
30
+ Use detail proportionate to the work: retain high-level design, low-level controls, and unresolved risks when they materially affect the result, while omitting routine implementation noise. Approved work-item titles and statements are retained in Shifu's private claims, so review their wording as durable content.
18
31
 
19
32
  Do not install or rely on lifecycle hooks or background reminders. A sync is always initiated by an explicit user request.
20
33
 
21
- Use this structure:
34
+ Use this structure. Do not invent additional evidence fields; the sync API rejects fields outside its contract.
22
35
 
23
36
  ```json
24
37
  {