@useshifu/coding-harness 0.3.6 → 0.3.8

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
@@ -20,7 +20,7 @@ For named meeting transcripts, set your display name in Shifu Profile so your sp
20
20
 
21
21
  The shortcuts are symlinks to the same folder, not additional watched folders. An existing conflicting `Shifu` item is left untouched and reported. To choose another existing, readable folder, use `--input-folder <path>` with `connect` or `config update`; Shifu checks the path before saving the change. No macOS permission flow is required by the CLI.
22
22
 
23
- No content is sent during setup. Every `connect` asks for a connection key and verifies it with Shifu for the chosen coding tool before saving it or reporting success. An invalid key or unreachable server leaves the existing connection unchanged. Replacing a key resets local checkpoints so existing work can be reconsidered for the new connection; the selected sync policy remains. If a coding tool cannot run `npx` or accept hidden input in its tool terminal, run the same command in your own interactive terminal and check the confirmation output. Change the choices later with:
23
+ No content is sent during setup. Every `connect` asks for a connection key using a normal terminal prompt, so the key is visible as you type or paste it. The key is verified with Shifu for the chosen coding tool before saving it or reporting success. An invalid key or unreachable server leaves the existing connection unchanged. Replacing a key resets local checkpoints so existing work can be reconsidered for the new connection; the selected sync policy remains. If a coding tool cannot run `npx` or accept input in its tool terminal, run the same command in your own interactive terminal and check the confirmation output. Change the choices later with:
24
24
 
25
25
  ```sh
26
26
  npx --yes @useshifu/coding-harness config update --harness <codex|claude_code|opencode> --interval-hours 1 --approval-mode automatic --input-folder ~/Desktop/Shifu
@@ -41,13 +41,19 @@ npx --yes @useshifu/coding-harness approve --harness codex
41
41
  npx --yes @useshifu/coding-harness sync-files --harness codex
42
42
  ```
43
43
 
44
+ To select by session count instead of a time window, run `sessions --harness codex --count 5`. It selects the five most recently updated eligible sessions, newest first, without an hours cutoff. Current and setup sessions are excluded. `selectedSessionRefs` fixes that selection; `sessions` contains pending segments and `skippedSessions` reports already-synced sessions. Skipped sessions are not replaced with older ones. Review each pending session with `review --harness codex --session-ref <sessionRef> --all`. After an accepted segment, request the next with `sessions --harness codex --session-ref <sessionRef> --all` until that selected session is exhausted. Keep the original references instead of selecting another five.
45
+
46
+ Manual session sync runs immediately, independently of the hourly schedule. If a segment fails, report it as failed/pending; a checkpoint advances only after a matching server receipt. Report completed, already-synced, and failed session counts separately.
47
+
48
+ To upgrade an existing installation, run `npx --yes @useshifu/coding-harness@latest install --harness codex` (or `claude_code` / `opencode`). It replaces the local runner and harness instruction while preserving the connection, checkpoints, and schedule. Reconnecting is unnecessary. `status` includes `runnerVersion` so you can verify the installed version.
49
+
44
50
  Automatic scheduled runs send each accepted segment and advance its local checkpoint only after the server returns a matching receipt. Manual scheduled runs rebuild an accumulated local queue without advancing checkpoints and create or resume one content-free harness session when the pending count changes; one approval sends the queue in order. Network failures and server throttling are retried, while rejected segments stay pending.
45
51
 
46
52
  The same scheduled run checks the input folder. `sync-files` checks it immediately without changing the schedule. Before sending new or changed files, the runner polls unfinished jobs from its local checkpoints. It sends supported file bytes over the configured HTTPS connection (or localhost HTTP for development). The server accepts each file as a background job and processes its text in chunks; upload does not wait for claim extraction. A later scheduled or manual sync polls the job's result. Only `processed`, `no_claims`, or `duplicate` finalizes the local checkpoint; `partial` reopens failed chunks on the next sync. On this upgrade, completed file checkpoints are replayed once so the new extraction can retain suggestions; in-flight jobs are polled first and replayed after finishing. Server-side idempotency prevents already published claims from being duplicated. Files larger than 20 MB remain pending with an error. The scanner reads regular files directly in the chosen folder and ignores symlinks and subfolders. Unknown formats are reported as unsupported rather than treated as successful syncs.
47
53
 
48
54
  The file-job result reports published claims separately from needs-evidence suggestions. A `no_claims` result can still contain suggestions: these are possible contributions awaiting evidence or attribution, not verified claims. The runner reports the suggestion count but never labels those suggestions as established work. It does not print the file's text, candidate statements, or source excerpts to logs. Use Shifu to inspect the suggestions and their evidence status. A result with zero claims and zero suggestions means no user-attributable contribution was extracted; it does not mean the upload failed.
49
55
 
50
- The connector captures supported harness work in engineering, product discovery, writing, interview feedback, research, and planning. Automatic sync turns completed actions and known work topics into separate controlled statements without sending source prose. If a segment has no supported work items, it advances the checkpoint without creating a claim. Version 1–3 clients remain supported.
56
+ The connector captures supported harness work in engineering, product discovery, writing, interview feedback, research, and planning. Automatic sync turns completed actions, known work topics, descriptive feature or project types, and supported problem or approach details into separate controlled statements without sending source prose or raw names. If a segment has no supported work items, it advances the checkpoint without creating a claim. Version 1–3 clients remain supported.
51
57
 
52
58
  ## Sync a prepared payload
53
59
 
@@ -6,7 +6,6 @@ const os = require("node:os");
6
6
  const path = require("node:path");
7
7
  const { execFileSync } = require("node:child_process");
8
8
  const readline = require("node:readline/promises");
9
- const tty = require("node:tty");
10
9
 
11
10
  const DEFAULT_API_URL = "https://api.useshifu.com";
12
11
  const HARNESS_NAMES = {
@@ -38,6 +37,7 @@ const MAX_TURNS_PER_SEGMENT = 12;
38
37
  const MAX_TITLE_LENGTH = 160;
39
38
  const MAX_WORK_DETAIL_LENGTH = 600;
40
39
  const CONFIG_VERSION = 2;
40
+ const RUNNER_VERSION = "0.3.8";
41
41
  const REDACTION_VERSION = 4;
42
42
  const DEFAULT_INTERVAL_HOURS = 1;
43
43
  const LEGACY_INTERVAL_HOURS = 4;
@@ -588,23 +588,107 @@ const WORK_OBJECTS = [
588
588
  { key: "planning", title: "project plan", pattern: /\b(?:roadmap|project plan|delivery plan)\b/i },
589
589
  ];
590
590
 
591
+ // Only controlled feature descriptions leave the device during unattended sync.
592
+ const WORK_CONTEXTS = [
593
+ { key: "payment_reconciliation", title: "payment reconciliation flow", phrase: "a payment reconciliation flow", pattern: /\bpayment reconciliation\b/i },
594
+ { key: "payment", title: "payment flow", phrase: "a payment flow", pattern: /\b(?:payment|payments|payout|payouts)\b/i },
595
+ { key: "checkout", title: "checkout flow", phrase: "a checkout flow", pattern: /\bcheckout\b/i },
596
+ { key: "billing", title: "billing flow", phrase: "a billing flow", pattern: /\b(?:billing|invoic(?:e|es|ing)|subscription|subscriptions)\b/i },
597
+ { key: "reconciliation", title: "reconciliation flow", phrase: "a reconciliation flow", pattern: /\breconciliation\b/i },
598
+ { key: "onboarding", title: "onboarding flow", phrase: "an onboarding flow", pattern: /\bonboarding\b/i },
599
+ { key: "search", title: "search experience", phrase: "a search experience", pattern: /\bsearch\b/i },
600
+ { key: "notification", title: "notification flow", phrase: "a notification flow", pattern: /\bnotifications?\b/i },
601
+ { key: "reporting", title: "reporting feature", phrase: "a reporting feature", pattern: /\b(?:reporting|reports)\b/i },
602
+ { key: "messaging", title: "messaging feature", phrase: "a messaging feature", pattern: /\bmessaging\b/i },
603
+ { key: "file_upload", title: "file upload flow", phrase: "a file upload flow", pattern: /\b(?:file uploads?|document uploads?)\b/i },
604
+ { key: "scheduling", title: "scheduling flow", phrase: "a scheduling flow", pattern: /\b(?:scheduling|booking)\b/i },
605
+ { key: "analytics", title: "analytics feature", phrase: "an analytics feature", pattern: /\banalytics\b/i },
606
+ { key: "data_import", title: "data import flow", phrase: "a data import flow", pattern: /\b(?:data import|csv import|bulk import)\b/i },
607
+ { key: "document_processing", title: "document processing flow", phrase: "a document processing flow", pattern: /\bdocument processing\b/i },
608
+ ];
609
+
610
+ const WORK_DETAILS = [
611
+ { title: "timeouts", pattern: /\btimeouts?\b/i },
612
+ { title: "duplicates", pattern: /\bduplicat(?:e|es|ed|ion)\b/i },
613
+ { title: "stale data", pattern: /\bstale (?:data|results?|cache)\b/i },
614
+ { title: "race conditions", pattern: /\brace conditions?\b/i },
615
+ { title: "data loss", pattern: /\bdata loss\b/i },
616
+ { title: "latency", pattern: /\b(?:latency|slow responses?)\b/i },
617
+ { title: "idempotency", pattern: /\bidempoten(?:t|cy)\b/i },
618
+ { title: "backoff", pattern: /\bbackoff\b/i },
619
+ { title: "validation", pattern: /\bvalidat(?:e|ed|ion|ing)\b/i },
620
+ { title: "pagination", pattern: /\bpaginat(?:e|ed|ion|ing)\b/i },
621
+ { title: "batching", pattern: /\bbatch(?:es|ed|ing)?\b/i },
622
+ { title: "rate limiting", pattern: /\brate limit(?:s|ing|ed)?\b/i },
623
+ { title: "deduplication", pattern: /\bdedupl?icat(?:e|ed|ion|ing)\b/i },
624
+ { title: "fallback behavior", pattern: /\bfallback\b/i },
625
+ { title: "accessibility", pattern: /\baccessibility\b/i },
626
+ { title: "mobile layout", pattern: /\bmobile layout\b/i },
627
+ ];
628
+
629
+ function controlledContexts(text) {
630
+ const matches = WORK_CONTEXTS.flatMap((item) => {
631
+ const match = item.pattern.exec(text);
632
+ if (!match) return [];
633
+ const type = text.slice(match.index + match[0].length).match(/^\s+(service|platform|system|feature|project|workflow|flow|experience)\b/i)?.[1];
634
+ if (!type) return [item];
635
+ const title = `${item.title.replace(/ (?:flow|feature|experience)$/, "")} ${type.toLowerCase()}`;
636
+ return [{ ...item, title, phrase: `${item.phrase.startsWith("an ") ? "an" : "a"} ${title}` }];
637
+ });
638
+ const contexts = matches.some((item) => item.key === "payment_reconciliation") ? matches.filter((item) => item.key !== "payment" && item.key !== "reconciliation") : matches;
639
+ return contexts.length <= 2 ? contexts : null;
640
+ }
641
+
642
+ function controlledDetails(text) {
643
+ return WORK_DETAILS.map((item) => ({ item, index: item.pattern.exec(text)?.index }))
644
+ .filter(({ index }) => index !== undefined)
645
+ .sort((left, right) => left.index - right.index)
646
+ .slice(0, 3)
647
+ .map(({ item }) => item.title);
648
+ }
649
+
650
+ function withoutNamedLabels(text) {
651
+ return text.replace(/\b(?:Project|Client|Customer|Company)\s+[A-Z][A-Za-z0-9_-]*/g, "");
652
+ }
653
+
591
654
  function safeActivityEvidence(notes) {
592
655
  const evidence = [];
593
656
  const seen = new Set();
594
657
  for (const [noteIndex, { turn, note }] of notes.entries()) {
595
658
  let inCodeBlock = false;
659
+ let sectionContexts = [];
596
660
  for (const [lineIndex, rawLine] of note.split("\n").entries()) {
597
661
  if (rawLine.trim().startsWith("```")) { inCodeBlock = !inCodeBlock; continue; }
598
662
  if (inCodeBlock) continue;
663
+ const heading = rawLine.trim().match(/^(?:#{1,6}\s+(.+)|\*\*(.+)\*\*:?\s*)$/);
664
+ if (heading) {
665
+ const label = withoutNamedLabels(heading[1] || heading[2]);
666
+ sectionContexts = /\b(?:project|feature|flow|service|system|product)\b/i.test(label) ? controlledContexts(label) || [] : [];
667
+ continue;
668
+ }
599
669
  const line = rawLine.trim().replace(/^[-*]\s+/, "").split(/[.;—]/, 1)[0];
670
+ const clauses = line.split(/\s+(?:and|but|while)\s+/i);
671
+ const nextAction = clauses.findIndex((clause, index) => index > 0 && WORK_ACTIONS.some((item) => item.pattern.test(clause)));
672
+ const workClause = withoutNamedLabels(nextAction > 0 ? clauses.slice(0, nextAction).join(" and ") : line);
600
673
  const action = WORK_ACTIONS.find((item) => item.pattern.test(line));
601
- const object = WORK_OBJECTS.find((item) => item.pattern.test(line));
674
+ const detailSource = workClause.split(/\b(?:about|titled)\b/i, 1)[0];
675
+ const contexts = controlledContexts(detailSource);
676
+ const featureContexts = contexts === null ? [] : contexts.length ? contexts : sectionContexts;
677
+ let object;
678
+ let objectPosition = Infinity;
679
+ for (const item of WORK_OBJECTS) {
680
+ const match = item.pattern.exec(workClause);
681
+ if (match && match.index < objectPosition) { object = item; objectPosition = match.index; }
682
+ }
683
+ object ||= featureContexts[0];
602
684
  if (!action || !object) continue;
603
- const key = `${action.key}:${object.key}`;
604
- if (seen.has(key)) continue;
605
- seen.add(key);
606
- const title = `${action.title} ${object.title}`;
607
- evidence.push({ kind: "activity", title, statement: `${title}.`, itemRef: `t${turn}_n${noteIndex + 1}_l${lineIndex + 1}` });
685
+ const context = ["interview_feedback", "professional_post"].includes(object.key) ? [] : featureContexts.filter((item) => item !== object);
686
+ const details = controlledDetails(detailSource).filter((detail) => !object.title.includes(detail));
687
+ const title = `${action.title} ${object.title}${context.length ? ` for ${context.map((item) => item.phrase).join(" and ")}` : ""}`;
688
+ const statement = `${title}${details.length ? ` (${details.join(", ")})` : ""}.`;
689
+ if (seen.has(statement)) continue;
690
+ seen.add(statement);
691
+ evidence.push({ kind: "activity", title, statement, itemRef: `t${turn}_n${noteIndex + 1}_l${lineIndex + 1}` });
608
692
  if (evidence.length === MAX_EVIDENCE_ITEMS) return evidence;
609
693
  }
610
694
  }
@@ -615,8 +699,8 @@ function payloadForSegment(harness, segment) {
615
699
  const { notes } = reviewSegment(harness, segment.sessionRef, segment.fromTurn, segment.toTurn);
616
700
  const evidence = safeActivityEvidence(notes);
617
701
  const title = evidence.length ? evidence[0].title : "No supported work identified";
618
- const summary = evidence.length > 1 ? `${evidence[0].statement} ${evidence.length - 1} more supported work items.` :
619
- evidence.length === 1 ? evidence[0].statement : "This range had no supported work items.";
702
+ const summary = evidence.length ? `${evidence.slice(0, 3).map((item) => item.statement).join(" ")}${evidence.length > 3 ? ` ${evidence.length - 3} more supported work items.` : ""}` :
703
+ "This range had no supported work items.";
620
704
  return {
621
705
  harness,
622
706
  sessionRef: segment.sessionRef,
@@ -754,8 +838,9 @@ function validateGeneratedProfile(profile, harness) {
754
838
  }
755
839
  }
756
840
 
757
- function unsyncedCodexSessions(harness, hours, turns = MAX_TURNS_PER_SEGMENT, selectedRef, includeOlder = false) {
758
- if (![24, 48, 72].includes(hours)) throw new Error("Choose --hours 24, 48, or 72.");
841
+ function selectedSessionSegments(harness, { hours = 24, turns = MAX_TURNS_PER_SEGMENT, selectedRef, includeOlder = false, count } = {}) {
842
+ if (count === undefined && ![24, 48, 72].includes(hours)) throw new Error("Choose --hours 24, 48, or 72.");
843
+ if (count !== undefined && (!Number.isSafeInteger(count) || count < 1)) throw new Error("Choose a positive whole number for --count.");
759
844
  if (!Number.isInteger(turns) || turns < 1 || turns > MAX_TURNS_PER_SEGMENT) throw new Error("Choose --turns between 1 and 12.");
760
845
  const since = Date.now() - hours * 3_600_000;
761
846
  const state = readState(harness);
@@ -765,7 +850,7 @@ function unsyncedCodexSessions(harness, hours, turns = MAX_TURNS_PER_SEGMENT, se
765
850
  const sessions = new Map();
766
851
  for (const session of localSessions(harness)
767
852
  .filter((session) => !excluded.has(session.sessionRef))
768
- .filter((session) => selectedRef ? session.sessionRef === selectedRef : Date.parse(session.updatedAt) >= since)) {
853
+ .filter((session) => selectedRef ? session.sessionRef === selectedRef : count !== undefined || Date.parse(session.updatedAt) >= since)) {
769
854
  const existing = sessions.get(session.sessionRef);
770
855
  sessions.set(session.sessionRef, existing ? {
771
856
  ...session,
@@ -776,7 +861,10 @@ function unsyncedCodexSessions(harness, hours, turns = MAX_TURNS_PER_SEGMENT, se
776
861
  } : session);
777
862
  }
778
863
  return [...sessions.values()]
779
- .sort((left, right) => left.updatedAt.localeCompare(right.updatedAt))
864
+ .sort((left, right) => count === undefined
865
+ ? left.updatedAt.localeCompare(right.updatedAt)
866
+ : right.updatedAt.localeCompare(left.updatedAt) || left.sessionRef.localeCompare(right.sessionRef))
867
+ .slice(0, count ?? Infinity)
780
868
  .map(({ turnTimes, ...session }) => {
781
869
  const checkpoint = state.sessions[session.sessionRef]?.lastSyncedTurn || 0;
782
870
  return {
@@ -787,7 +875,11 @@ function unsyncedCodexSessions(harness, hours, turns = MAX_TURNS_PER_SEGMENT, se
787
875
  oldestUnsyncedAt: turnTimes[checkpoint] || null,
788
876
  };
789
877
  })
790
- .filter((session) => session.fromTurn <= session.toTurn && (includeOlder || Date.parse(session.oldestUnsyncedAt) >= since));
878
+ .filter((session) => count !== undefined || includeOlder || Date.parse(session.oldestUnsyncedAt) >= since);
879
+ }
880
+
881
+ function unsyncedCodexSessions(harness, options = {}) {
882
+ return selectedSessionSegments(harness, options).filter((session) => session.fromTurn <= session.toTurn);
791
883
  }
792
884
 
793
885
  function pendingSegments(harness) {
@@ -833,41 +925,17 @@ async function confirm(question) {
833
925
  }
834
926
  }
835
927
 
836
- async function readSecret(input = process.stdin) {
837
- const ownsInput = !input.isTTY;
838
- if (ownsInput) {
839
- let descriptor;
840
- try { descriptor = fs.openSync(process.platform === "win32" ? "CONIN$" : "/dev/tty", "r"); }
841
- catch { throw new Error("This terminal cannot accept a hidden connection key. Run connect in your own interactive terminal."); }
842
- input = new tty.ReadStream(descriptor);
928
+ async function readSecret(input = process.stdin, output = process.stderr) {
929
+ const terminal = readline.createInterface({ input, output });
930
+ try {
931
+ const answer = terminal.question("Connection key: ");
932
+ const closed = new Promise((_, reject) => terminal.once("close", () => reject(new Error("Connection key input closed."))));
933
+ return await Promise.race([answer, closed]);
934
+ } catch {
935
+ throw new Error("Could not read a connection key. Run connect in an interactive terminal.");
936
+ } finally {
937
+ terminal.close();
843
938
  }
844
- const wasRaw = input.isRaw;
845
- process.stderr.write("Connection key (hidden): ");
846
- input.setRawMode(true);
847
- input.setEncoding("utf8");
848
- return new Promise((resolve, reject) => {
849
- let value = "";
850
- const finish = (error) => {
851
- input.off("data", onData);
852
- input.off("error", onError);
853
- input.setRawMode(Boolean(wasRaw));
854
- if (ownsInput) input.destroy();
855
- process.stderr.write("\n");
856
- if (error) reject(error);
857
- else resolve(value);
858
- };
859
- const onError = (error) => finish(error);
860
- const onData = (chunk) => {
861
- for (const character of chunk) {
862
- if (character === "\r" || character === "\n") return finish();
863
- if (character === "\u0003") return finish(new Error("Connection was cancelled."));
864
- if (character === "\u007f" || character === "\b") value = value.slice(0, -1);
865
- else if (character >= " " && character !== "\u007f") value += character;
866
- }
867
- };
868
- input.on("data", onData);
869
- input.on("error", onError);
870
- });
871
939
  }
872
940
 
873
941
  function copyRunner() {
@@ -953,7 +1021,9 @@ function install(harness, quiet = false) {
953
1021
  if (!quiet) {
954
1022
  console.error(`Installed the Shifu sync instruction for ${HARNESS_NAMES[harness]}.`);
955
1023
  if (removedHooks) console.error("Removed obsolete Codex lifecycle hooks.");
956
- console.error(`Next: npx --yes @useshifu/coding-harness connect --harness ${harness}`);
1024
+ console.error(fs.existsSync(configPath(harness))
1025
+ ? "Updated the local runner and sync instruction. Your connection, checkpoints, and schedule were preserved."
1026
+ : `Next: npx --yes @useshifu/coding-harness connect --harness ${harness}`);
957
1027
  }
958
1028
  }
959
1029
 
@@ -1696,7 +1766,7 @@ function status(harness) {
1696
1766
  if (folder) {
1697
1767
  try { unsupportedFiles = scanWorkFiles(folder).unsupported; } catch (error) { inputFolderError = error.message; }
1698
1768
  }
1699
- console.log(JSON.stringify({ harness, apiUrl: config.apiUrl, inputFolder: folder, inputFolderError, syncPolicy: normalizedPolicy(config), schedule: state.schedule || null, pendingSegments: pending.segments.length, completedFiles: entries.filter((entry) => typeof entry === "string").length, pendingFiles: entries.filter((entry) => typeof entry === "object" && entry?.jobId).length, unsupportedFiles, sessions: state.sessions }, null, 2));
1769
+ console.log(JSON.stringify({ harness, runnerVersion: RUNNER_VERSION, apiUrl: config.apiUrl, inputFolder: folder, inputFolderError, syncPolicy: normalizedPolicy(config), schedule: state.schedule || null, pendingSegments: pending.segments.length, completedFiles: entries.filter((entry) => typeof entry === "string").length, pendingFiles: entries.filter((entry) => typeof entry === "object" && entry?.jobId).length, unsupportedFiles, sessions: state.sessions }, null, 2));
1700
1770
  }
1701
1771
 
1702
1772
  async function syncFilesNow(harness) {
@@ -1708,16 +1778,30 @@ async function syncFilesNow(harness) {
1708
1778
 
1709
1779
  function sessions(harness) {
1710
1780
  readConfig(harness);
1781
+ const count = option("--count");
1782
+ if (hasFlag("--count") && count === undefined) throw new Error("Provide a value for --count.");
1783
+ if (count !== undefined && option("--hours") !== undefined) throw new Error("Use either --count or --hours, not both.");
1711
1784
  const hours = numberOption("--hours", 24);
1712
1785
  const turns = numberOption("--turns", MAX_TURNS_PER_SEGMENT);
1713
1786
  const sessionRef = option("--session-ref");
1714
1787
  const all = hasFlag("--all");
1788
+ if (count !== undefined && (sessionRef || all)) throw new Error("Use --count without --session-ref or --all.");
1715
1789
  if (all && !sessionRef) throw new Error("Use --all only with an explicitly selected --session-ref.");
1716
- const pending = unsyncedCodexSessions(harness, hours, turns, sessionRef, all);
1717
- const blocked = all ? [] : unsyncedCodexSessions(harness, hours, turns, sessionRef, true)
1790
+ const countValue = count === undefined ? undefined : Number(count);
1791
+ const selected = selectedSessionSegments(harness, { hours, turns, selectedRef: sessionRef, includeOlder: all, count: countValue });
1792
+ const pending = selected.filter((session) => session.fromTurn <= session.toTurn);
1793
+ const blocked = all || count !== undefined ? [] : unsyncedCodexSessions(harness, { hours, turns, selectedRef: sessionRef, includeOlder: true })
1718
1794
  .filter((session) => !pending.some((item) => item.sessionRef === session.sessionRef))
1719
1795
  .map(({ sessionRef, oldestUnsyncedAt }) => ({ sessionRef, oldestUnsyncedAt }));
1720
- console.log(JSON.stringify({ harness, hours, turns, sessions: pending, blockedSessions: blocked }, null, 2));
1796
+ console.log(JSON.stringify(count === undefined
1797
+ ? { harness, hours, turns, sessions: pending, blockedSessions: blocked }
1798
+ : {
1799
+ harness, count: countValue, order: "latest_to_earliest", turns,
1800
+ selectedSessionRefs: selected.map((session) => session.sessionRef),
1801
+ sessions: pending,
1802
+ skippedSessions: selected.filter((session) => session.fromTurn > session.toTurn)
1803
+ .map(({ sessionRef }) => ({ sessionRef, reason: "already_synced" })),
1804
+ }, null, 2));
1721
1805
  }
1722
1806
 
1723
1807
  function review(harness) {
@@ -10,10 +10,11 @@ If manual approval has queued scheduled work, run `pending --harness opencode`,
10
10
 
11
11
  For an explicit sync-now request:
12
12
 
13
- 1. Run `sessions --harness opencode --hours 24`, using 48 or 72 only when requested.
14
- 2. Review each returned segment with `review --harness opencode --session-ref <sessionRef> --hours <hours>`.
15
- 3. Prepare a redaction-version-four payload from final assistant notes only. Use `activity` evidence for writing, feedback, research, planning, product work, and any work that is not clearly an engineering implementation or decision. Activity evidence has no technical scope. An empty evidence list advances the checkpoint without creating a claim.
13
+ 1. For a time-window request, run `sessions --harness opencode --hours 24`, using 48 or 72 only when requested. For a request by number of recent sessions, run `sessions --harness opencode --count <count>`; this ignores the hours window and returns the most recently updated sessions first.
14
+ 2. Review each returned segment with `review --harness opencode --session-ref <sessionRef> --hours <hours>`. For count-based selection, use `review --harness opencode --session-ref <sessionRef> --all` so older sessions are allowed.
15
+ 3. Prepare a redaction-version-four payload from final assistant notes only. Use `activity` evidence for writing, feedback, research, planning, product work, and any work that is not clearly an engineering implementation or decision. Activity evidence has no technical scope. Preserve the supported action, feature or project purpose, specific problem, approach, and verified result. Synthesize a descriptive project label (such as "payment reconciliation service") from its purpose; never copy a raw label that could identify a company or person. Keep distinct contributions separate, including different fixes within one feature. An empty evidence list advances the checkpoint without creating a claim.
16
16
  4. In automatic mode, pipe the payload to `sync --harness opencode` without asking again. In manual mode, show the exact payload, ask once, and then use `sync --approved --harness opencode`.
17
- 5. Keep the returned session reference and turn range unchanged. Never upload prompts, raw transcripts, tool output, code, paths, commands, URLs, credentials, names, customer details, or proprietary identifiers.
17
+ 5. Keep the returned session reference and turn range unchanged. Never upload prompts, raw transcripts, tool output, code, paths, commands, URLs, credentials, company or person names, customer details, or proprietary identifiers.
18
+ 6. For a count-based sync, keep `selectedSessionRefs` from the first discovery and review only pending `sessions`. `skippedSessions` reports already-synced selections; do not replace them with older sessions. If one has remaining turns, request its next segment with `sessions --harness opencode --session-ref <sessionRef> --all`. Stop after those selected sessions are exhausted. Report completed, already-synced, and failed/pending sessions separately. Manual sync runs immediately, independently of the hourly schedule.
18
19
 
19
- To connect or replace a key, run `npx --yes @useshifu/coding-harness connect --harness opencode`. Connect asks for a key and verifies it with Shifu before reporting success. If this tool terminal cannot run the command or accept hidden input, ask the user to run it in their own interactive terminal and report the output; do not claim the connection succeeded. To disconnect locally, run `npx --yes @useshifu/coding-harness disconnect --harness opencode`. New connections use one-hour automatic sync and `~/Desktop/Shifu` on macOS by default. To change the interval, approval policy, or folder later, run `config update --harness opencode` with the desired flags (for example, `--input-folder <existing-readable-folder>`).
20
+ To connect or replace a key, run `npx --yes @useshifu/coding-harness connect --harness opencode`. Connect asks for a key with a normal visible terminal prompt and verifies it with Shifu before reporting success. Do not ask the user to paste the key into chat. If this tool terminal cannot run the command or accept input, ask the user to run it in their own interactive terminal and report the output; do not claim the connection succeeded. To disconnect locally, run `npx --yes @useshifu/coding-harness disconnect --harness opencode`. New connections use one-hour automatic sync and `~/Desktop/Shifu` on macOS by default. To change the interval, approval policy, or folder later, run `config update --harness opencode` with the desired flags (for example, `--input-folder <existing-readable-folder>`).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@useshifu/coding-harness",
3
- "version": "0.3.6",
3
+ "version": "0.3.8",
4
4
  "description": "Scheduled, redacted coding-harness activity sync for Shifu",
5
5
  "bin": {
6
6
  "coding-harness": "bin/shifu-harness.js",
@@ -7,7 +7,7 @@ description: Connect, configure, inspect, or sync coding-harness activity, AI Pr
7
7
 
8
8
  Shifu records useful work completed through the harness. Work may be engineering, product discovery, writing, interview feedback, research, planning, or another user-directed activity. Do not force non-engineering work into engineering categories.
9
9
 
10
- Never include user prompts, raw transcripts, tool output, source code, commands, credentials, URLs, file paths, names, customer details, or proprietary identifiers in a coding-session payload. The local coding runner reads final assistant notes for career activity and scans user turns locally for fixed AI Profile signals; it sends neither raw source. Files placed in the user's chosen Shifu input folder are a separate path: the runner sends supported files over TLS for server-side parsing and sanitization. Do not describe folder files as locally redacted.
10
+ Never include user prompts, raw transcripts, tool output, source code, commands, credentials, URLs, file paths, company or person names, customer details, or proprietary identifiers in a coding-session payload. Synthesize descriptive project labels from their purpose rather than copying a raw name. The local coding runner reads final assistant notes for career activity and scans user turns locally for fixed AI Profile signals; it sends neither raw source. Files placed in the user's chosen Shifu input folder are a separate path: the runner sends supported files over TLS for server-side parsing and sanitization. Do not describe folder files as locally redacted.
11
11
 
12
12
  Only describe work attributable to the user. A team action item, meeting discussion, or another person's contribution is not evidence that the user did it. If a final note does not support a specific user contribution, do not invent one.
13
13
 
@@ -23,7 +23,7 @@ When the user asks to connect, run:
23
23
  npx --yes @useshifu/coding-harness connect --harness codex
24
24
  ```
25
25
 
26
- `connect` asks for a key every time and verifies it against Shifu for this harness before reporting success. If this tool terminal cannot run npx or accept hidden input, ask the user to run the command in their own interactive terminal and report its output; do not claim the connection succeeded. It installs the current runner and a local schedule with no policy questions. New connections default to hourly automatic sync and `~/Desktop/Shifu` as the single input folder on macOS. It creates shortcuts at `~/Downloads/Shifu` and `~/Documents/Shifu` when possible without replacing conflicting items. Supported files are `.txt`, `.md`, `.rtf`, and `.docx` up to 20 MB directly inside the folder; images and screenshots are not supported. Unknown formats are counted but never uploaded. Replacing a key resets local checkpoints so work can sync to the new connection. It also excludes the setup session when the harness exposes its current opaque session reference. No content is sent during setup.
26
+ `connect` asks for a key with a normal visible terminal prompt every time and verifies it against Shifu for this harness before reporting success. Do not ask the user to paste the key into chat. If this tool terminal cannot run npx or accept input, ask the user to run the command in their own interactive terminal and report its output; do not claim the connection succeeded. It installs the current runner and a local schedule with no policy questions. New connections default to hourly automatic sync and `~/Desktop/Shifu` as the single input folder on macOS. It creates shortcuts at `~/Downloads/Shifu` and `~/Documents/Shifu` when possible without replacing conflicting items. Supported files are `.txt`, `.md`, `.rtf`, and `.docx` up to 20 MB directly inside the folder; images and screenshots are not supported. Unknown formats are counted but never uploaded. Replacing a key resets local checkpoints so work can sync to the new connection. It also excludes the setup session when the harness exposes its current opaque session reference. No content is sent during setup.
27
27
 
28
28
  To disconnect locally, run `npx --yes @useshifu/coding-harness disconnect --harness codex`. Tell the user to revoke the old key in Shifu if they no longer want it usable.
29
29
 
@@ -53,17 +53,19 @@ Show the returned list and ask for one approval for the whole list. After approv
53
53
 
54
54
  ## Sync now
55
55
 
56
- For an explicit sync request, first read `status` to determine the saved approval mode. Use the time window the user names; otherwise use 24 hours. Supported interactive windows are 24, 48, and 72 hours.
56
+ For an explicit sync request, first read `status` to determine the saved approval mode. Manual sync runs immediately and does not wait for the hourly schedule. Use the time window the user names; otherwise use 24 hours. Supported interactive windows are 24, 48, and 72 hours. If the user specifies a number of recent sessions, use count-based selection instead; it has no hours cutoff and orders sessions from latest to earliest. It selects the latest eligible sessions, excluding current/setup sessions, and reports already-synced selections in `skippedSessions` without replacing them with older sessions.
57
57
 
58
- 1. Run `npx --yes @useshifu/coding-harness sessions --harness codex --hours <24|48|72>`.
58
+ 1. For a time-window sync, run `npx --yes @useshifu/coding-harness sessions --harness codex --hours <24|48|72>`. For a count-based sync, run `npx --yes @useshifu/coding-harness sessions --harness codex --count <count>`; do not add `--hours`.
59
59
  2. Ignore `blockedSessions` unless the user specifically selects one. For a selected older backlog, add `--session-ref <sessionRef> --all`.
60
- 3. For each returned segment, run `npx --yes @useshifu/coding-harness review --harness codex --session-ref <sessionRef> --hours <24|48|72>`.
60
+ 3. For each returned segment, run `npx --yes @useshifu/coding-harness review --harness codex --session-ref <sessionRef> --hours <24|48|72>`. For count-based selection, use `--all` instead of `--hours` so the review has no time cutoff.
61
61
  4. Prepare a version-four payload from the returned final-assistant notes. Keep the returned `sessionRef`, `fromTurn`, and `toTurn` unchanged.
62
62
  5. In automatic mode, send it without another approval. In manual mode, show the exact payload and ask once; after approval, pass `--approved` to the sync command.
63
- 6. Repeat discovery after accepted segments until the selected window is exhausted.
63
+ 6. For a time-window sync, repeat discovery after accepted segments until the selected window is exhausted. For a count-based sync, keep `selectedSessionRefs` from the first discovery and review only pending `sessions`. If a selected session has remaining turns, request its next segment with `sessions --harness codex --session-ref <sessionRef> --all`; stop after those selected sessions are exhausted. Report completed, already-synced, and failed/pending sessions separately. A failed session does not count as completed.
64
64
 
65
65
  Use `kind: "activity"` for general harness work; it does not require a technical scope. Use `implementation` or `decision` only when the note clearly supports that engineering classification, and then use the smallest reviewed scope among `unit`, `module`, `service`, `system`, and `product`. Evidence may be empty when the safe final note supports only a session-level title and summary. Never invent impact, ownership, verification, or outcomes.
66
66
 
67
+ Make each supported item useful for later recall: retain the user's action, the feature or project purpose, the specific problem, the approach, and a verified result when the final note supports each part. A descriptive project label such as "payment reconciliation service" is useful; synthesize it from the work's purpose instead of copying a raw project label that might contain a company or person's name. Keep distinct contributions separate, including two fixes in one feature that addressed different problems. Omit names of companies and people, customer details, proprietary identifiers, paths, code snippets, and unsupported impact. If a detail cannot be safely described, keep the supported parts. Prefer a summary that names the main distinct work items over a generic count alone.
68
+
67
69
  ```json
68
70
  {
69
71
  "harness": "codex",