@useshifu/coding-harness 0.2.6 → 0.2.7
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 +4 -4
- package/bin/shifu-harness.js +58 -3
- package/package.json +1 -1
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 connector has no lifecycle hooks and never uploads in the background. Codex and OpenCode support local time-window discovery and review
|
|
10
|
+
`connect` and `sync` each ask for confirmation. The connector has no lifecycle hooks and never uploads in the background. Codex, Claude Code, and OpenCode support local time-window discovery and review. Claude Code discovery reads local sessions under `~/.claude/projects`; review surfaces assistant text for inspection, never raw prompts or tool results.
|
|
11
11
|
|
|
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`.
|
|
12
|
+
When asked to sync, Codex, Claude Code, 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|opencode> --hours 24
|
|
16
|
-
npx @useshifu/coding-harness review --harness <codex|opencode> --session-ref opaque-session-id
|
|
15
|
+
npx @useshifu/coding-harness sessions --harness <codex|claude_code|opencode> --hours 24
|
|
16
|
+
npx @useshifu/coding-harness review --harness <codex|claude_code|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.
|
package/bin/shifu-harness.js
CHANGED
|
@@ -53,6 +53,10 @@ function codexSessionsRoot() {
|
|
|
53
53
|
return process.env.SHIFU_CODEX_SESSIONS_ROOT || path.join(os.homedir(), ".codex", "sessions");
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
function claudeSessionsRoot() {
|
|
57
|
+
return process.env.SHIFU_CLAUDE_SESSIONS_ROOT || path.join(os.homedir(), ".claude", "projects");
|
|
58
|
+
}
|
|
59
|
+
|
|
56
60
|
function opencodeQuery(query) {
|
|
57
61
|
if (process.env.SHIFU_OPENCODE_DB && !process.env.SHIFU_OPENCODE_BIN) {
|
|
58
62
|
try {
|
|
@@ -116,8 +120,9 @@ function opencodeSessions(rows) {
|
|
|
116
120
|
|
|
117
121
|
function localSessions(harness) {
|
|
118
122
|
if (harness === "codex") return sessionFiles(codexSessionsRoot()).map(codexSession).filter(Boolean);
|
|
123
|
+
if (harness === "claude_code") return sessionFiles(claudeSessionsRoot()).map(claudeSession).filter(Boolean);
|
|
119
124
|
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("
|
|
125
|
+
throw new Error("Choose a supported coding harness.");
|
|
121
126
|
}
|
|
122
127
|
|
|
123
128
|
function requireHarness(value) {
|
|
@@ -218,6 +223,52 @@ function codexSession(file) {
|
|
|
218
223
|
return { sessionRef, startedAt: startedAt || updatedAt, updatedAt, turnCount, turnTimes };
|
|
219
224
|
}
|
|
220
225
|
|
|
226
|
+
function claudeEntries(file) {
|
|
227
|
+
return fs.readFileSync(file, "utf8").split("\n").filter(Boolean).map((line) => {
|
|
228
|
+
try { return JSON.parse(line); }
|
|
229
|
+
catch { throw new Error(`Could not read Claude Code session metadata from ${file}.`); }
|
|
230
|
+
}).filter((entry) => !entry.isSidechain);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function isClaudeUserTurn(entry) {
|
|
234
|
+
if (entry.type !== "user" || entry.message?.role !== "user") return false;
|
|
235
|
+
const content = entry.message.content;
|
|
236
|
+
return typeof content === "string" || (Array.isArray(content) && content.some((part) => part.type === "text"));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function claudeSession(file) {
|
|
240
|
+
const sessionRef = path.basename(file, ".jsonl");
|
|
241
|
+
if (!OPAQUE_SESSION_REF_PATTERN.test(sessionRef)) return undefined;
|
|
242
|
+
let startedAt;
|
|
243
|
+
let updatedAt;
|
|
244
|
+
const turnTimes = [];
|
|
245
|
+
for (const entry of claudeEntries(file)) {
|
|
246
|
+
if (typeof entry.timestamp !== "string" || !Number.isFinite(Date.parse(entry.timestamp))) continue;
|
|
247
|
+
startedAt ||= entry.timestamp;
|
|
248
|
+
updatedAt = entry.timestamp;
|
|
249
|
+
if (isClaudeUserTurn(entry)) turnTimes.push(entry.timestamp);
|
|
250
|
+
}
|
|
251
|
+
return updatedAt && turnTimes.length ? { sessionRef, startedAt, updatedAt, turnCount: turnTimes.length, turnTimes } : undefined;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function claudeReview(file, fromTurn, toTurn) {
|
|
255
|
+
const sessionRef = path.basename(file, ".jsonl");
|
|
256
|
+
let turn = 0;
|
|
257
|
+
const lastNoteByTurn = new Map();
|
|
258
|
+
for (const entry of claudeEntries(file)) {
|
|
259
|
+
if (isClaudeUserTurn(entry)) {
|
|
260
|
+
turn += 1;
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
if (entry.type !== "assistant" || entry.message?.role !== "assistant" || turn < fromTurn || turn > toTurn) continue;
|
|
264
|
+
const content = entry.message.content;
|
|
265
|
+
if (!Array.isArray(content) || content.some((part) => part.type === "tool_use")) continue;
|
|
266
|
+
const note = content.filter((part) => part.type === "text" && typeof part.text === "string").map((part) => part.text).join("\n").trim();
|
|
267
|
+
if (note) lastNoteByTurn.set(turn, note);
|
|
268
|
+
}
|
|
269
|
+
return { sessionRef, turnCount: turn, notes: [...lastNoteByTurn].map(([t, note]) => ({ turn: t, note })) };
|
|
270
|
+
}
|
|
271
|
+
|
|
221
272
|
function finalAssistantNote(file) {
|
|
222
273
|
const lines = fs.readFileSync(file, "utf8").split("\n").filter(Boolean);
|
|
223
274
|
let sessionRef;
|
|
@@ -490,6 +541,7 @@ function install(harness) {
|
|
|
490
541
|
const content = fs.readFileSync(skill, "utf8")
|
|
491
542
|
.replaceAll('"codex"', `"${harness}"`)
|
|
492
543
|
.replaceAll("--harness codex", `--harness ${harness}`)
|
|
544
|
+
.replaceAll("Codex", HARNESS_NAMES[harness])
|
|
493
545
|
.replaceAll("npx @useshifu/coding-harness", `"${process.execPath}" "${installedRunnerPath()}"`);
|
|
494
546
|
fs.writeFileSync(skill, content, { mode: 0o600 });
|
|
495
547
|
}
|
|
@@ -631,7 +683,10 @@ function review(harness) {
|
|
|
631
683
|
if (!Number.isInteger(turns) || turns < 1 || turns > MAX_TURNS_PER_SEGMENT) throw new Error("Choose --turns between 1 and 12.");
|
|
632
684
|
const toTurn = Math.min(selected.turnCount, fromTurn + turns - 1);
|
|
633
685
|
if (fromTurn > toTurn) throw new Error(`This ${HARNESS_NAMES[harness]} session has no unsynced turns to review.`);
|
|
634
|
-
const
|
|
686
|
+
const files = harness === "codex" ? sessionFiles(codexSessionsRoot()) : harness === "claude_code" ? sessionFiles(claudeSessionsRoot()) : [];
|
|
687
|
+
const selectedFile = files.map((file) => ({ file, session: harness === "codex" ? codexSession(file) : claudeSession(file) }))
|
|
688
|
+
.filter(({ session }) => session?.sessionRef === sessionRef).sort((left, right) => right.session.turnCount - left.session.turnCount)[0]?.file;
|
|
689
|
+
const { notes } = harness === "codex" ? codexReview(selectedFile, fromTurn, toTurn) : harness === "claude_code" ? claudeReview(selectedFile, fromTurn, toTurn) : opencodeReview(sessionRef, fromTurn, toTurn);
|
|
635
690
|
const candidates = reviewCandidates(notes);
|
|
636
691
|
console.log(JSON.stringify({
|
|
637
692
|
sessionRef,
|
|
@@ -669,4 +724,4 @@ if (require.main === module) {
|
|
|
669
724
|
});
|
|
670
725
|
}
|
|
671
726
|
|
|
672
|
-
module.exports = { codexReview, codexSession, configPath, finalAssistantNote, install, installDestination, localSessions, opencodeQuery, opencodeReview, opencodeSessions, readSecret, removeHooks, requireSafeApiUrl, reviewCandidates, statePath, textIsSafe, unsyncedCodexSessions, validSyncReceipt, validateSync };
|
|
727
|
+
module.exports = { claudeReview, claudeSession, codexReview, codexSession, configPath, finalAssistantNote, install, installDestination, localSessions, opencodeQuery, opencodeReview, opencodeSessions, readSecret, removeHooks, requireSafeApiUrl, reviewCandidates, statePath, textIsSafe, unsyncedCodexSessions, validSyncReceipt, validateSync };
|