@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 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; Claude Code does not yet.
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.
@@ -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("Session discovery is currently available for Codex and OpenCode only.");
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 { 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);
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@useshifu/coding-harness",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "description": "Reviewed, incremental coding-harness sync for Shifu",
5
5
  "bin": {
6
6
  "coding-harness": "bin/shifu-harness.js",