@useshifu/coding-harness 0.2.2 → 0.2.5

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
 
@@ -50,6 +51,73 @@ function codexSessionsRoot() {
50
51
  return process.env.SHIFU_CODEX_SESSIONS_ROOT || path.join(os.homedir(), ".codex", "sessions");
51
52
  }
52
53
 
54
+ function opencodeQuery(query) {
55
+ if (process.env.SHIFU_OPENCODE_DB && !process.env.SHIFU_OPENCODE_BIN) {
56
+ try {
57
+ const sqliteOutput = execFileSync("sqlite3", ["-json", process.env.SHIFU_OPENCODE_DB, query], {
58
+ encoding: "utf8",
59
+ stdio: ["ignore", "pipe", "pipe"],
60
+ maxBuffer: 64 * 1024 * 1024,
61
+ });
62
+ return JSON.parse(sqliteOutput);
63
+ } catch {
64
+ throw new Error("Could not read OpenCode session metadata. Ensure the opencode command is installed and its local database is available.");
65
+ }
66
+ }
67
+ const temporary = path.join(os.tmpdir(), `shifu-opencode-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.json`);
68
+ let fd;
69
+ try {
70
+ fd = fs.openSync(temporary, "w");
71
+ execFileSync(process.env.SHIFU_OPENCODE_BIN || "opencode", ["db", query, "--format", "json"], {
72
+ stdio: ["ignore", fd, "pipe"],
73
+ maxBuffer: 64 * 1024 * 1024,
74
+ });
75
+ fs.closeSync(fd);
76
+ fd = undefined;
77
+ return JSON.parse(fs.readFileSync(temporary, "utf8"));
78
+ } catch {
79
+ if (fd !== undefined) {
80
+ try { fs.closeSync(fd); } catch {}
81
+ fd = undefined;
82
+ }
83
+ const dbPath = process.env.SHIFU_OPENCODE_DB || path.join(process.env.XDG_DATA_HOME || path.join(os.homedir(), ".local", "share"), "opencode", "opencode.db");
84
+ if (fs.existsSync(dbPath)) {
85
+ try {
86
+ const sqliteOutput = execFileSync("sqlite3", ["-json", dbPath, query], {
87
+ encoding: "utf8",
88
+ stdio: ["ignore", "pipe", "pipe"],
89
+ maxBuffer: 64 * 1024 * 1024,
90
+ });
91
+ return JSON.parse(sqliteOutput);
92
+ } catch {}
93
+ }
94
+ throw new Error("Could not read OpenCode session metadata. Ensure the opencode command is installed and its local database is available.");
95
+ } finally {
96
+ if (fd !== undefined) {
97
+ try { fs.closeSync(fd); } catch {}
98
+ }
99
+ try { fs.unlinkSync(temporary); } catch {}
100
+ }
101
+ }
102
+
103
+ function opencodeSessions(rows) {
104
+ const sessions = new Map();
105
+ for (const row of rows) {
106
+ if (typeof row.sessionRef !== "string" || !Number.isFinite(row.startedAt) || !Number.isFinite(row.updatedAt) || !Number.isFinite(row.turnAt)) continue;
107
+ const session = sessions.get(row.sessionRef) || { sessionRef: row.sessionRef, startedAt: new Date(row.startedAt).toISOString(), updatedAt: new Date(row.updatedAt).toISOString(), turnCount: 0, turnTimes: [] };
108
+ session.turnCount += 1;
109
+ session.turnTimes.push(new Date(row.turnAt).toISOString());
110
+ sessions.set(row.sessionRef, session);
111
+ }
112
+ return [...sessions.values()];
113
+ }
114
+
115
+ function localSessions(harness) {
116
+ if (harness === "codex") return sessionFiles(codexSessionsRoot()).map(codexSession).filter(Boolean);
117
+ 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"));
118
+ throw new Error("Session discovery is currently available for Codex and OpenCode only.");
119
+ }
120
+
53
121
  function requireHarness(value) {
54
122
  if (!Object.hasOwn(HARNESS_NAMES, value)) throw new Error("Choose --harness codex, claude_code, or opencode.");
55
123
  return value;
@@ -203,6 +271,46 @@ function codexReview(file, fromTurn, toTurn) {
203
271
  return { sessionRef, turnCount: turn, notes };
204
272
  }
205
273
 
274
+ function opencodeReview(sessionRef, fromTurn, toTurn) {
275
+ 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`);
276
+ const messages = new Map();
277
+ for (const row of rows) {
278
+ const message = messages.get(row.messageId) || { role: row.role, finish: row.finish, parts: [] };
279
+ if (typeof row.part === "string") {
280
+ try {
281
+ const part = JSON.parse(row.part);
282
+ if (part.type === "text" && typeof part.text === "string") message.parts.push(part.text);
283
+ } catch {}
284
+ }
285
+ messages.set(row.messageId, message);
286
+ }
287
+ let turn = 0;
288
+ const turnMessages = new Map();
289
+ for (const message of messages.values()) {
290
+ if (message.role === "user") {
291
+ turn += 1;
292
+ turnMessages.set(turn, []);
293
+ continue;
294
+ }
295
+ if (message.role === "assistant" && turn > 0) {
296
+ turnMessages.get(turn).push(message);
297
+ }
298
+ }
299
+ const notes = [];
300
+ for (const [t, msgs] of turnMessages.entries()) {
301
+ if (t < fromTurn || t > toTurn) continue;
302
+ let candidates = msgs.filter((m) => m.finish === "stop" || (m.finish && m.finish !== "tool-calls"));
303
+ if (candidates.length === 0) {
304
+ candidates = msgs.filter((m) => m.parts.length > 0).slice(-1);
305
+ }
306
+ for (const m of candidates) {
307
+ const note = m.parts.join("\n").trim();
308
+ if (note) notes.push({ turn: t, note });
309
+ }
310
+ }
311
+ return { sessionRef, turnCount: turn, notes };
312
+ }
313
+
206
314
  function reviewCandidates(notes) {
207
315
  const candidates = [];
208
316
  const seen = new Set();
@@ -224,15 +332,12 @@ function reviewCandidates(notes) {
224
332
  }
225
333
 
226
334
  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
335
  if (![24, 48, 72].includes(hours)) throw new Error("Choose --hours 24, 48, or 72.");
229
336
  if (!Number.isInteger(turns) || turns < 1 || turns > MAX_TURNS_PER_SEGMENT) throw new Error("Choose --turns between 1 and 12.");
230
337
  const since = Date.now() - hours * 3_600_000;
231
338
  const state = readState(harness);
232
339
  const sessions = new Map();
233
- for (const session of sessionFiles(codexSessionsRoot())
234
- .map(codexSession)
235
- .filter(Boolean)
340
+ for (const session of localSessions(harness)
236
341
  .filter((session) => selectedRef ? session.sessionRef === selectedRef : Date.parse(session.updatedAt) >= since)) {
237
342
  const existing = sessions.get(session.sessionRef);
238
343
  sessions.set(session.sessionRef, existing ? {
@@ -353,16 +458,31 @@ function removeCodexHooks() {
353
458
  function installDestination(harness) {
354
459
  if (harness === "codex") return path.join(os.homedir(), ".codex", "skills", "shifu-sync");
355
460
  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");
461
+ return path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"), "opencode", "skills", "shifu-sync");
357
462
  }
358
463
 
359
464
  function install(harness) {
360
465
  copyRunner();
361
- const destination = installDestination(harness);
362
466
  if (harness === "opencode") {
363
- fs.mkdirSync(path.dirname(destination), { recursive: true, mode: 0o700 });
364
- fs.copyFileSync(path.join(__dirname, "..", "commands", "shifu-sync.md"), destination);
467
+ const configDir = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
468
+ const skillDest = path.join(configDir, "opencode", "skills", "shifu-sync");
469
+ fs.mkdirSync(skillDest, { recursive: true, mode: 0o700 });
470
+ const skillContent = fs.readFileSync(path.join(__dirname, "..", "skills", "shifu-sync", "SKILL.md"), "utf8")
471
+ .replaceAll('"codex"', '"opencode"')
472
+ .replaceAll("--harness codex", "--harness opencode")
473
+ .replaceAll("Codex", "OpenCode")
474
+ .replaceAll("npx @useshifu/coding-harness", `"${process.execPath}" "${installedRunnerPath()}"`);
475
+ fs.writeFileSync(path.join(skillDest, "SKILL.md"), skillContent, { mode: 0o600 });
476
+
477
+ const commandContent = fs.readFileSync(path.join(__dirname, "..", "commands", "shifu-sync.md"), "utf8")
478
+ .replaceAll("npx @useshifu/coding-harness", `"${process.execPath}" "${installedRunnerPath()}"`);
479
+ for (const folder of ["command", "commands"]) {
480
+ const commandDir = path.join(configDir, "opencode", folder);
481
+ fs.mkdirSync(commandDir, { recursive: true, mode: 0o700 });
482
+ fs.writeFileSync(path.join(commandDir, "shifu-sync.md"), commandContent, { mode: 0o600 });
483
+ }
365
484
  } else {
485
+ const destination = installDestination(harness);
366
486
  copyDirectory(path.join(__dirname, "..", "skills", "shifu-sync"), destination);
367
487
  const skill = path.join(destination, "SKILL.md");
368
488
  const content = fs.readFileSync(skill, "utf8")
@@ -479,51 +599,48 @@ function sessions(harness) {
479
599
  }
480
600
 
481
601
  function review(harness) {
482
- if (harness !== "codex") throw new Error("Session review is currently available for Codex only.");
483
602
  readConfig(harness);
484
603
  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.");
604
+ if (typeof sessionRef !== "string" || sessionRef.length < 8) throw new Error(`Provide an opaque ${HARNESS_NAMES[harness]} --session-ref from the sessions command.`);
486
605
  const state = readState(harness);
487
606
  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.");
607
+ const selected = localSessions(harness).filter((session) => session.sessionRef === sessionRef)
608
+ .sort((left, right) => right.turnCount - left.turnCount || right.updatedAt.localeCompare(left.updatedAt))[0];
609
+ if (!selected) throw new Error(`No local ${HARNESS_NAMES[harness]} session matches this reference.`);
494
610
  const hours = numberOption("--hours", 24);
495
611
  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)) {
612
+ if (!hasFlag("--all") && !(Date.parse(selected.turnTimes[fromTurn - 1]) >= Date.now() - hours * 3_600_000)) {
497
613
  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
614
  }
499
615
  const turns = numberOption("--turns", MAX_TURNS_PER_SEGMENT);
500
616
  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);
617
+ const toTurn = Math.min(selected.turnCount, fromTurn + turns - 1);
618
+ if (fromTurn > toTurn) throw new Error(`This ${HARNESS_NAMES[harness]} session has no unsynced turns to review.`);
619
+ 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
620
  const candidates = reviewCandidates(notes);
505
621
  console.log(JSON.stringify({
506
622
  sessionRef,
507
623
  fromTurn,
508
624
  toTurn,
509
- oldestUnsyncedAt: selected.session.turnTimes[fromTurn - 1] || null,
625
+ oldestUnsyncedAt: selected.turnTimes[fromTurn - 1] || null,
510
626
  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
627
  notes,
512
628
  candidates,
513
629
  draft: { title: null, summary: null, evidence: candidates.map((candidate) => ({ ...candidate, title: null })) },
514
630
  candidateOverflow: candidates.length > MAX_EVIDENCE_ITEMS,
515
- remainingTurns: selected.session.turnCount - toTurn,
631
+ remainingTurns: selected.turnCount - toTurn,
516
632
  }, null, 2));
517
633
  }
518
634
 
519
635
  async function main() {
520
636
  const command = process.argv[2];
637
+ const subCommand = process.argv[3];
521
638
  const harness = requireHarness(option("--harness"));
522
639
  if (command === "install") return install(harness);
523
640
  if (command === "connect") return connect(harness);
524
641
  if (command === "sync") return sync(harness);
525
642
  if (command === "status") return status(harness);
526
- if (command === "sessions") return sessions(harness);
643
+ if (command === "sessions" || command === "discover" || (command === "session" && (subCommand === "discover" || subCommand === "list" || !subCommand || subCommand.startsWith("-")))) return sessions(harness);
527
644
  if (command === "review") return review(harness);
528
645
  throw new Error("Use install, connect, status, sessions, review, or sync.");
529
646
  }
@@ -537,4 +654,4 @@ if (require.main === module) {
537
654
  });
538
655
  }
539
656
 
540
- module.exports = { codexReview, codexSession, configPath, finalAssistantNote, readSecret, removeHooks, requireSafeApiUrl, reviewCandidates, statePath, textIsSafe, unsyncedCodexSessions, validSyncReceipt, validateSync };
657
+ 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.5",
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,7 +14,20 @@ 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