@useshifu/coding-harness 0.3.7 → 0.3.9

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
@@ -41,6 +41,12 @@ 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.
@@ -37,6 +37,7 @@ const MAX_TURNS_PER_SEGMENT = 12;
37
37
  const MAX_TITLE_LENGTH = 160;
38
38
  const MAX_WORK_DETAIL_LENGTH = 600;
39
39
  const CONFIG_VERSION = 2;
40
+ const RUNNER_VERSION = "0.3.9";
40
41
  const REDACTION_VERSION = 4;
41
42
  const DEFAULT_INTERVAL_HOURS = 1;
42
43
  const LEGACY_INTERVAL_HOURS = 4;
@@ -837,8 +838,9 @@ function validateGeneratedProfile(profile, harness) {
837
838
  }
838
839
  }
839
840
 
840
- function unsyncedCodexSessions(harness, hours, turns = MAX_TURNS_PER_SEGMENT, selectedRef, includeOlder = false) {
841
- 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.");
842
844
  if (!Number.isInteger(turns) || turns < 1 || turns > MAX_TURNS_PER_SEGMENT) throw new Error("Choose --turns between 1 and 12.");
843
845
  const since = Date.now() - hours * 3_600_000;
844
846
  const state = readState(harness);
@@ -848,7 +850,7 @@ function unsyncedCodexSessions(harness, hours, turns = MAX_TURNS_PER_SEGMENT, se
848
850
  const sessions = new Map();
849
851
  for (const session of localSessions(harness)
850
852
  .filter((session) => !excluded.has(session.sessionRef))
851
- .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)) {
852
854
  const existing = sessions.get(session.sessionRef);
853
855
  sessions.set(session.sessionRef, existing ? {
854
856
  ...session,
@@ -859,7 +861,10 @@ function unsyncedCodexSessions(harness, hours, turns = MAX_TURNS_PER_SEGMENT, se
859
861
  } : session);
860
862
  }
861
863
  return [...sessions.values()]
862
- .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)
863
868
  .map(({ turnTimes, ...session }) => {
864
869
  const checkpoint = state.sessions[session.sessionRef]?.lastSyncedTurn || 0;
865
870
  return {
@@ -870,7 +875,11 @@ function unsyncedCodexSessions(harness, hours, turns = MAX_TURNS_PER_SEGMENT, se
870
875
  oldestUnsyncedAt: turnTimes[checkpoint] || null,
871
876
  };
872
877
  })
873
- .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);
874
883
  }
875
884
 
876
885
  function pendingSegments(harness) {
@@ -1012,7 +1021,9 @@ function install(harness, quiet = false) {
1012
1021
  if (!quiet) {
1013
1022
  console.error(`Installed the Shifu sync instruction for ${HARNESS_NAMES[harness]}.`);
1014
1023
  if (removedHooks) console.error("Removed obsolete Codex lifecycle hooks.");
1015
- 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}`);
1016
1027
  }
1017
1028
  }
1018
1029
 
@@ -1755,7 +1766,7 @@ function status(harness) {
1755
1766
  if (folder) {
1756
1767
  try { unsupportedFiles = scanWorkFiles(folder).unsupported; } catch (error) { inputFolderError = error.message; }
1757
1768
  }
1758
- 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));
1759
1770
  }
1760
1771
 
1761
1772
  async function syncFilesNow(harness) {
@@ -1767,16 +1778,30 @@ async function syncFilesNow(harness) {
1767
1778
 
1768
1779
  function sessions(harness) {
1769
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.");
1770
1784
  const hours = numberOption("--hours", 24);
1771
1785
  const turns = numberOption("--turns", MAX_TURNS_PER_SEGMENT);
1772
1786
  const sessionRef = option("--session-ref");
1773
1787
  const all = hasFlag("--all");
1788
+ if (count !== undefined && (sessionRef || all)) throw new Error("Use --count without --session-ref or --all.");
1774
1789
  if (all && !sessionRef) throw new Error("Use --all only with an explicitly selected --session-ref.");
1775
- const pending = unsyncedCodexSessions(harness, hours, turns, sessionRef, all);
1776
- 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 })
1777
1794
  .filter((session) => !pending.some((item) => item.sessionRef === session.sessionRef))
1778
1795
  .map(({ sessionRef, oldestUnsyncedAt }) => ({ sessionRef, oldestUnsyncedAt }));
1779
- 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));
1780
1805
  }
1781
1806
 
1782
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>`.
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
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
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
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.7",
3
+ "version": "0.3.9",
4
4
  "description": "Scheduled, redacted coding-harness activity sync for Shifu",
5
5
  "bin": {
6
6
  "coding-harness": "bin/shifu-harness.js",
@@ -53,14 +53,14 @@ 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