@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 +8 -4
- package/bin/shifu-harness.js +141 -24
- package/commands/shifu-sync.md +38 -2
- package/package.json +1 -1
- package/skills/shifu-sync/SKILL.md +15 -2
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
|
|
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
|
|
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`.
|
package/bin/shifu-harness.js
CHANGED
|
@@ -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
|
|
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", "
|
|
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
|
-
|
|
364
|
-
|
|
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(
|
|
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
|
|
489
|
-
.
|
|
490
|
-
|
|
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.
|
|
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.
|
|
502
|
-
if (fromTurn > toTurn) throw new Error(
|
|
503
|
-
const { notes } = codexReview(
|
|
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.
|
|
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.
|
|
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 };
|
package/commands/shifu-sync.md
CHANGED
|
@@ -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
|
|
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`,
|
|
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: shifu-sync
|
|
3
|
-
description: Prepare and sync reviewed, redacted Codex work from the user's selected recent time window to Shifu. Use
|
|
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.
|
|
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
|
|