@useshifu/coding-harness 0.3.3 → 0.3.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 CHANGED
@@ -5,7 +5,7 @@
5
5
  ## Connect
6
6
 
7
7
  ```sh
8
- npx @useshifu/coding-harness connect --harness <codex|claude_code|opencode>
8
+ npx --yes @useshifu/coding-harness connect --harness <codex|claude_code|opencode>
9
9
  ```
10
10
 
11
11
  Connect installs the local instruction and scheduled runner, saves the connection key with owner-only permissions, and uses these defaults without setup questions:
@@ -20,35 +20,39 @@ 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. Re-running `connect` on an older installation keeps its key, checkpoints, and selected sync policy. Change the choices later with:
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:
24
24
 
25
25
  ```sh
26
- npx @useshifu/coding-harness config update --harness <codex|claude_code|opencode> --interval-hours 1 --approval-mode automatic --input-folder ~/Desktop/Shifu
26
+ npx --yes @useshifu/coding-harness config update --harness <codex|claude_code|opencode> --interval-hours 1 --approval-mode automatic --input-folder ~/Desktop/Shifu
27
27
  ```
28
28
 
29
29
  Older configuration files remain on-demand and manual until either command completes onboarding; upgrading the package alone never enables background upload.
30
30
 
31
+ To disconnect this device, run `npx --yes @useshifu/coding-harness disconnect --harness <codex|claude_code|opencode>`. This removes the local key, schedule, and checkpoints. It does not revoke the key on Shifu; revoke the old key there if it should no longer work.
32
+
31
33
  ## Operate
32
34
 
33
35
  ```sh
34
- npx @useshifu/coding-harness status --harness codex
35
- npx @useshifu/coding-harness sessions --harness codex --hours 24
36
- npx @useshifu/coding-harness review --harness codex --session-ref opaque-session-id
37
- npx @useshifu/coding-harness pending --harness codex
38
- npx @useshifu/coding-harness approve --harness codex
39
- npx @useshifu/coding-harness sync-files --harness codex
36
+ npx --yes @useshifu/coding-harness status --harness codex
37
+ npx --yes @useshifu/coding-harness sessions --harness codex --hours 24
38
+ npx --yes @useshifu/coding-harness review --harness codex --session-ref opaque-session-id
39
+ npx --yes @useshifu/coding-harness pending --harness codex
40
+ npx --yes @useshifu/coding-harness approve --harness codex
41
+ npx --yes @useshifu/coding-harness sync-files --harness codex
40
42
  ```
41
43
 
42
44
  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.
43
45
 
44
- 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. Existing file checkpoints are checked once again after upgrading to this job-based protocol. 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.
46
+ 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
+
48
+ 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.
45
49
 
46
- The connector captures any useful harness activity, including engineering, product discovery, writing, interview feedback, research, and planning. Version-four payloads support generic `activity` items without a technical scope and accept a safe session-level summary when no item survives redaction. Version 1–3 clients remain supported.
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.
47
51
 
48
52
  ## Sync a prepared payload
49
53
 
50
54
  ```sh
51
- printf '%s' '{"harness":"codex","sessionRef":"opaque-session-id","fromTurn":1,"toTurn":3,"title":"Prepared structured interview feedback","summary":"Prepared structured interview feedback and clarified the recommendation.","evidence":[{"kind":"activity","title":"Drafted interview feedback","statement":"Prepared structured interview feedback."}],"verification":[],"decisions":[],"redactionVersion":4}' | npx @useshifu/coding-harness sync --harness codex
55
+ printf '%s' '{"harness":"codex","sessionRef":"opaque-session-id","fromTurn":1,"toTurn":3,"title":"Prepared structured interview feedback","summary":"Prepared structured interview feedback and clarified the recommendation.","evidence":[{"kind":"activity","title":"Drafted interview feedback","statement":"Prepared structured interview feedback."}],"verification":[],"decisions":[],"redactionVersion":4}' | npx --yes @useshifu/coding-harness sync --harness codex
52
56
  ```
53
57
 
54
58
  Automatic mode sends immediately. Manual mode asks once unless the calling harness already obtained approval and passes `--approved`.
@@ -42,7 +42,7 @@ const MAX_INTERVAL_HOURS = 720;
42
42
  const SCHEDULER_TICK_MINUTES = 15;
43
43
  const MAX_RETRIES = 4;
44
44
  const MAX_WORK_FILE_BYTES = 20 * 1024 * 1024;
45
- const WORK_FILE_SYNC_VERSION = 3;
45
+ const WORK_FILE_SYNC_VERSION = 4;
46
46
  const WORK_FILE_EXTENSIONS = new Set([".txt", ".md", ".rtf", ".docx"]);
47
47
  const FINAL_WORK_FILE_STATUSES = new Set(["processed", "no_claims", "duplicate"]);
48
48
  const ACTIVE_WORK_FILE_STATUSES = new Set(["queued", "processing", "retrying"]);
@@ -53,7 +53,7 @@ Put work logs, meeting transcripts, and other work documents directly in this fo
53
53
 
54
54
  Supported files: .txt, .md, .rtf, and .docx, up to 20 MB each. Subfolders, links, images, and screenshots are not processed. This README is not synced. Unsupported files are counted but never uploaded.
55
55
 
56
- Uploading starts a background job. Shifu checks unfinished jobs before the next scheduled or manual sync; a file is complete only when the server reports its final result. A partial result stays pending for retry.
56
+ Uploading starts a background job. Shifu checks unfinished jobs before the next scheduled or manual sync; a file is complete only when the server reports its final result. A partial result stays pending for retry. A completed file may have published claims, needs-evidence suggestions, or neither; suggestions are not verified claims.
57
57
 
58
58
  Files you add are sent to Shifu for parsing and sanitization; they are not redacted on this device. Only place files here if you want them processed. For named meeting transcripts, set your display name in Shifu Profile so your speaker turns can be matched.
59
59
  `;
@@ -543,41 +543,77 @@ function conciseTitle(statement, fallback) {
543
543
  return `${value.slice(0, MAX_TITLE_LENGTH - 1).trimEnd()}…`;
544
544
  }
545
545
 
546
- const ACTIVITY_CATEGORIES = [
547
- { key: "interview_feedback", title: "Interview feedback", pattern: /\b(?:interview|candidate|hiring feedback)\b/i },
548
- { key: "professional_writing", title: "Professional writing", pattern: /\b(?:linkedin|social post|article|newsletter|professional post|writing)\b/i },
549
- { key: "product_discovery", title: "Product discovery", pattern: /\b(?:product discovery|user research|customer research|requirements?|roadmap)\b/i },
550
- { key: "product_delivery", title: "Product and design delivery", pattern: /\b(?:landing page|website|user interface|user experience|design|prototype)\b/i },
551
- { key: "research", title: "Research", pattern: /\b(?:research|investigat|compar|evaluat|analysis|analyz)\w*/i },
552
- { key: "planning", title: "Planning", pattern: /\b(?:plan|strategy|prioriti|proposal|brief)\w*/i },
553
- { key: "documentation", title: "Documentation", pattern: /\b(?:documentation|readme|guide|runbook)\b/i },
554
- { key: "engineering", title: "Engineering", pattern: /\b(?:implement|code|bug|fix|test|api|database|frontend|backend|refactor|deploy)\w*/i },
546
+ const WORK_ACTIONS = [
547
+ { key: "implemented", title: "Implemented", pattern: /^(?:I\s+|We\s+)?(?:implemented|added|built|shipped)\b/i },
548
+ { key: "fixed", title: "Fixed", pattern: /^(?:I\s+|We\s+)?(?:fixed|resolved|corrected)\b/i },
549
+ { key: "improved", title: "Improved", pattern: /^(?:I\s+|We\s+)?(?:improved|optimized|updated)\b/i },
550
+ { key: "refactored", title: "Refactored", pattern: /^(?:I\s+|We\s+)?refactored\b/i },
551
+ { key: "designed", title: "Designed", pattern: /^(?:I\s+|We\s+)?designed\b/i },
552
+ { key: "prepared", title: "Prepared", pattern: /^(?:I\s+|We\s+)?(?:prepared|drafted|wrote)\b/i },
553
+ { key: "reviewed", title: "Reviewed", pattern: /^(?:I\s+|We\s+)?(?:reviewed|audited)\b/i },
554
+ { key: "investigated", title: "Investigated", pattern: /^(?:I\s+|We\s+)?(?:investigated|researched|evaluated|compared)\b/i },
555
+ { key: "deployed", title: "Deployed", pattern: /^(?:I\s+|We\s+)?(?:deployed|published)\b/i },
555
556
  ];
556
557
 
557
- function safeActivityEvidence(notes, segment) {
558
- const source = notes.map((note) => note.note).join("\n");
559
- const categories = ACTIVITY_CATEGORIES.filter((category) => category.pattern.test(source));
560
- if (categories.length === 0) return [];
561
- return categories.map((category, index) => ({
562
- kind: "activity",
563
- title: category.title,
564
- statement: `Used the harness for ${category.title.toLowerCase()}.`,
565
- itemRef: `t${segment.fromTurn}_${category.key}_${index + 1}`,
566
- }));
558
+ const WORK_OBJECTS = [
559
+ { key: "interview_feedback", title: "interview feedback", pattern: /\b(?:interview feedback|candidate feedback|hiring feedback)\b/i },
560
+ { key: "professional_post", title: "professional writing", pattern: /\b(?:linkedin|social post|professional post|newsletter|article)\b/i },
561
+ { key: "home_review", title: "Home review", pattern: /\b(?:home review|work review|review on the home page)\b/i },
562
+ { key: "connection_key", title: "connection key validation", pattern: /\b(?:connection key|connector key|key validation)\b/i },
563
+ { key: "claim_extraction", title: "claim extraction", pattern: /\b(?:claim extraction|claim generation|claim matching)\b/i },
564
+ { key: "retry", title: "retry handling", pattern: /\b(?:retries|retry)\b/i },
565
+ { key: "cache", title: "cache behavior", pattern: /\b(?:cache|caching|ttl)\b/i },
566
+ { key: "navigation", title: "navigation", pattern: /\b(?:navigation|sidebar|sub.?nav)\b/i },
567
+ { key: "sync", title: "sync behavior", pattern: /\b(?:sync|synchronization|checkpoint)\b/i },
568
+ { key: "auth", title: "authentication", pattern: /\b(?:authentication|sign.?in|login)\b/i },
569
+ { key: "permissions", title: "access controls", pattern: /\b(?:authorization|permissions?|access control)\b/i },
570
+ { key: "database", title: "database migration", pattern: /\b(?:database migration|schema migration|postgres migration)\b/i },
571
+ { key: "api", title: "API endpoint", pattern: /\b(?:api|endpoint|http route)\b/i },
572
+ { key: "frontend", title: "frontend component", pattern: /\b(?:frontend|react component|ui component|user interface)\b/i },
573
+ { key: "test", title: "test coverage", pattern: /\b(?:test|tests|test suite|coverage)\b/i },
574
+ { key: "docs", title: "documentation", pattern: /\b(?:documentation|readme|guide|runbook)\b/i },
575
+ { key: "deploy", title: "deployment pipeline", pattern: /\b(?:deployment|deploy pipeline|ci pipeline|release pipeline)\b/i },
576
+ { key: "performance", title: "performance", pattern: /\b(?:performance|latency|throughput)\b/i },
577
+ { key: "security", title: "security controls", pattern: /\b(?:security|vulnerability|secret handling)\b/i },
578
+ { key: "research", title: "product research", pattern: /\b(?:product discovery|user research|customer research)\b/i },
579
+ { key: "planning", title: "project plan", pattern: /\b(?:roadmap|project plan|delivery plan)\b/i },
580
+ ];
581
+
582
+ function safeActivityEvidence(notes) {
583
+ const evidence = [];
584
+ const seen = new Set();
585
+ for (const [noteIndex, { turn, note }] of notes.entries()) {
586
+ let inCodeBlock = false;
587
+ for (const [lineIndex, rawLine] of note.split("\n").entries()) {
588
+ if (rawLine.trim().startsWith("```")) { inCodeBlock = !inCodeBlock; continue; }
589
+ if (inCodeBlock) continue;
590
+ const line = rawLine.trim().replace(/^[-*]\s+/, "").split(/[.;—]/, 1)[0];
591
+ const action = WORK_ACTIONS.find((item) => item.pattern.test(line));
592
+ const object = WORK_OBJECTS.find((item) => item.pattern.test(line));
593
+ if (!action || !object) continue;
594
+ const key = `${action.key}:${object.key}`;
595
+ if (seen.has(key)) continue;
596
+ seen.add(key);
597
+ const title = `${action.title} ${object.title}`;
598
+ evidence.push({ kind: "activity", title, statement: `${title}.`, itemRef: `t${turn}_n${noteIndex + 1}_l${lineIndex + 1}` });
599
+ if (evidence.length === MAX_EVIDENCE_ITEMS) return evidence;
600
+ }
601
+ }
602
+ return evidence;
567
603
  }
568
604
 
569
605
  function payloadForSegment(harness, segment) {
570
606
  const { notes } = reviewSegment(harness, segment.sessionRef, segment.fromTurn, segment.toTurn);
571
- const fallback = `${HARNESS_NAMES[harness]} activity across turns ${segment.fromTurn}–${segment.toTurn}.`;
572
- const evidence = safeActivityEvidence(notes, segment).slice(0, MAX_EVIDENCE_ITEMS);
573
- const title = evidence.length === 1 ? evidence[0].title : evidence.length > 1 ? "Multiple harness activities" : fallback;
574
- const summary = evidence.length ? `Used ${HARNESS_NAMES[harness]} for ${evidence.map((item) => item.title.toLowerCase()).join(", ")}.` : fallback;
607
+ const evidence = safeActivityEvidence(notes);
608
+ const title = evidence.length ? evidence[0].title : "No supported work identified";
609
+ const summary = evidence.length > 1 ? `${evidence[0].statement} ${evidence.length - 1} more supported work items.` :
610
+ evidence.length === 1 ? evidence[0].statement : "This range had no supported work items.";
575
611
  return {
576
612
  harness,
577
613
  sessionRef: segment.sessionRef,
578
614
  fromTurn: segment.fromTurn,
579
615
  toTurn: segment.toTurn,
580
- title: conciseTitle(title, fallback),
616
+ title: conciseTitle(title, "No supported work identified"),
581
617
  summary,
582
618
  evidence,
583
619
  verification: [],
@@ -667,7 +703,12 @@ async function confirm(question) {
667
703
 
668
704
  async function readSecret(input = process.stdin) {
669
705
  const ownsInput = !input.isTTY;
670
- if (ownsInput) input = new tty.ReadStream(fs.openSync(process.platform === "win32" ? "CONIN$" : "/dev/tty", "r"));
706
+ if (ownsInput) {
707
+ let descriptor;
708
+ try { descriptor = fs.openSync(process.platform === "win32" ? "CONIN$" : "/dev/tty", "r"); }
709
+ catch { throw new Error("This terminal cannot accept a hidden connection key. Run connect in your own interactive terminal."); }
710
+ input = new tty.ReadStream(descriptor);
711
+ }
671
712
  const wasRaw = input.isRaw;
672
713
  process.stderr.write("Connection key (hidden): ");
673
714
  input.setRawMode(true);
@@ -755,11 +796,11 @@ function install(harness, quiet = false) {
755
796
  .replaceAll('"codex"', '"opencode"')
756
797
  .replaceAll("--harness codex", "--harness opencode")
757
798
  .replaceAll("Codex", "OpenCode")
758
- .replaceAll("npx @useshifu/coding-harness", `"${process.execPath}" "${installedRunnerPath()}"`);
799
+ .replaceAll("npx --yes @useshifu/coding-harness", `"${process.execPath}" "${installedRunnerPath()}"`);
759
800
  fs.writeFileSync(path.join(skillDest, "SKILL.md"), skillContent, { mode: 0o600 });
760
801
 
761
802
  const commandContent = fs.readFileSync(path.join(__dirname, "..", "commands", "shifu-sync.md"), "utf8")
762
- .replaceAll("npx @useshifu/coding-harness", `"${process.execPath}" "${installedRunnerPath()}"`);
803
+ .replaceAll("npx --yes @useshifu/coding-harness", `"${process.execPath}" "${installedRunnerPath()}"`);
763
804
  for (const folder of ["command", "commands"]) {
764
805
  const commandDir = path.join(configDir, "opencode", folder);
765
806
  fs.mkdirSync(commandDir, { recursive: true, mode: 0o700 });
@@ -773,14 +814,14 @@ function install(harness, quiet = false) {
773
814
  .replaceAll('"codex"', `"${harness}"`)
774
815
  .replaceAll("--harness codex", `--harness ${harness}`)
775
816
  .replaceAll("Codex", HARNESS_NAMES[harness])
776
- .replaceAll("npx @useshifu/coding-harness", `"${process.execPath}" "${installedRunnerPath()}"`);
817
+ .replaceAll("npx --yes @useshifu/coding-harness", `"${process.execPath}" "${installedRunnerPath()}"`);
777
818
  fs.writeFileSync(skill, content, { mode: 0o600 });
778
819
  }
779
820
  const removedHooks = harness === "codex" && removeCodexHooks();
780
821
  if (!quiet) {
781
822
  console.error(`Installed the Shifu sync instruction for ${HARNESS_NAMES[harness]}.`);
782
823
  if (removedHooks) console.error("Removed obsolete Codex lifecycle hooks.");
783
- console.error(`Next: coding-harness connect --harness ${harness}`);
824
+ console.error(`Next: npx --yes @useshifu/coding-harness connect --harness ${harness}`);
784
825
  }
785
826
  }
786
827
 
@@ -849,6 +890,45 @@ function installSchedule(harness) {
849
890
  return artifact.file;
850
891
  }
851
892
 
893
+ function removeSchedule(harness) {
894
+ const artifact = schedulerArtifact(harness, process.env.SHIFU_PLATFORM || process.platform);
895
+ if (process.env.SHIFU_SKIP_SCHEDULER_ACTIVATION !== "1") {
896
+ if (process.platform === "darwin") {
897
+ const domain = `gui/${process.getuid()}`;
898
+ const label = `com.useshifu.coding-harness.${harness}`;
899
+ const scheduleIsLoaded = () => {
900
+ let listing;
901
+ try { listing = execFileSync("launchctl", ["list"], { encoding: "utf8" }); }
902
+ catch { throw new Error("Could not check the Shifu schedule. Disconnect was cancelled."); }
903
+ return listing.split(/\r?\n/).some((line) => line.trim().split(/\s+/).at(-1) === label);
904
+ };
905
+ if (scheduleIsLoaded()) {
906
+ try { execFileSync("launchctl", ["bootout", domain, artifact.file], { stdio: "ignore" }); }
907
+ catch { throw new Error("Could not stop the Shifu schedule. Disconnect was cancelled."); }
908
+ if (scheduleIsLoaded()) throw new Error("The Shifu schedule is still running. Disconnect was cancelled.");
909
+ }
910
+ } else if (process.platform === "linux") {
911
+ if (fs.existsSync(artifact.file)) {
912
+ const timer = `shifu-coding-harness-${harness}.timer`;
913
+ try { execFileSync("systemctl", ["--user", "disable", "--now", timer], { stdio: "ignore" }); }
914
+ catch { throw new Error("Could not stop the Shifu schedule. Disconnect was cancelled."); }
915
+ let state;
916
+ try { state = execFileSync("systemctl", ["--user", "show", "--property=ActiveState", "--value", timer], { encoding: "utf8" }).trim(); }
917
+ catch { throw new Error("Could not verify that the Shifu schedule stopped. Disconnect was cancelled."); }
918
+ if (state === "active" || state === "activating") throw new Error("The Shifu schedule is still running. Disconnect was cancelled.");
919
+ }
920
+ } else if (process.platform === "win32") {
921
+ if (fs.existsSync(artifact.file)) {
922
+ try { execFileSync("schtasks.exe", ["/Delete", "/TN", `Shifu coding harness ${harness}`, "/F"], { stdio: "ignore" }); }
923
+ catch { throw new Error("Could not stop the Shifu schedule. Disconnect was cancelled."); }
924
+ }
925
+ }
926
+ }
927
+ for (const item of [artifact, ...(artifact.extraFiles || [])]) {
928
+ try { fs.unlinkSync(item.file); } catch (error) { if (error?.code !== "ENOENT") throw error; }
929
+ }
930
+ }
931
+
852
932
  function scheduleNextRun(harness, intervalHours) {
853
933
  const state = readState(harness);
854
934
  state.schedule = { ...(state.schedule || {}), nextRunAt: Date.now() + intervalHours * 3_600_000 };
@@ -864,11 +944,28 @@ async function chooseSyncPolicy(existing) {
864
944
  };
865
945
  }
866
946
 
867
- async function connect(harness) {
947
+ async function validateConnection(apiUrl, token, harness) {
948
+ let response;
949
+ try {
950
+ response = await fetch(`${apiUrl}/v1/connectors/coding-sessions/connection?harness=${encodeURIComponent(harness)}`, {
951
+ headers: { Authorization: `Bearer ${token}` },
952
+ signal: AbortSignal.timeout(10_000),
953
+ });
954
+ } catch {
955
+ throw new Error("Could not reach Shifu to verify the connection key. Nothing was changed.");
956
+ }
957
+ if (response.status === 401 || response.status === 422) throw new Error("Shifu rejected this connection key for the selected coding tool. Nothing was changed.");
958
+ if (!response.ok) throw new Error(`Shifu could not verify the connection key (HTTP ${response.status}). Nothing was changed.`);
959
+ const result = await response.json().catch(() => null);
960
+ if (result?.data?.connected !== true) throw new Error("Shifu returned an invalid connection check. Nothing was changed.");
961
+ }
962
+
963
+ async function connect(harness, readKey = readSecret) {
868
964
  const existing = readJSON(configPath(harness), undefined);
869
- const token = existing?.token || (await readSecret()).trim();
965
+ const token = (await readKey()).trim();
870
966
  if (!TOKEN_PATTERN.test(token)) throw new Error("The connection key is invalid.");
871
967
  const apiUrl = requireSafeApiUrl(option("--api-url") || existing?.apiUrl || process.env.SHIFU_API_URL || DEFAULT_API_URL);
968
+ await validateConnection(apiUrl, token, harness);
872
969
  const savedPolicy = existing && normalizedPolicy(existing);
873
970
  const currentPolicy = savedPolicy?.onboardingComplete ? savedPolicy : { intervalHours: DEFAULT_INTERVAL_HOURS, approvalMode: "automatic", excludedSessionRefs: savedPolicy?.excludedSessionRefs || [] };
874
971
  const selected = await chooseSyncPolicy(currentPolicy);
@@ -878,6 +975,11 @@ async function connect(harness) {
878
975
  install(harness, true);
879
976
  const scheduleFile = installSchedule(harness);
880
977
  writeJSON(configPath(harness), { ...existing, version: CONFIG_VERSION, apiUrl, token, harness, syncPolicy, inputFolder });
978
+ if (existing?.token && existing.token !== token) {
979
+ for (const file of [statePath(harness), pendingPath(harness)]) {
980
+ try { fs.unlinkSync(file); } catch (error) { if (error?.code !== "ENOENT") throw error; }
981
+ }
982
+ }
881
983
  scheduleNextRun(harness, syncPolicy.intervalHours);
882
984
  console.error(`${HARNESS_NAMES[harness]} is connected. It will sync every ${syncPolicy.intervalHours} hours with ${syncPolicy.approvalMode} approval.`);
883
985
  console.error(`Shifu sync folder: ${inputFolder}. Place .txt, .md, .rtf, or .docx files there. Only grounded contributions will become claims.`);
@@ -885,6 +987,16 @@ async function connect(harness) {
885
987
  console.error(`Schedule installed at ${scheduleFile}. No content was sent during setup.`);
886
988
  }
887
989
 
990
+ function disconnect(harness) {
991
+ readConfig(harness);
992
+ if (fs.existsSync(lockPath(harness))) throw new Error("A Shifu sync is running. Try disconnecting again after it finishes.");
993
+ removeSchedule(harness);
994
+ for (const file of [configPath(harness), statePath(harness), pendingPath(harness)]) {
995
+ try { fs.unlinkSync(file); } catch (error) { if (error?.code !== "ENOENT") throw error; }
996
+ }
997
+ console.error(`${HARNESS_NAMES[harness]} is disconnected on this device. Revoke the old key in Shifu if you no longer want it usable.`);
998
+ }
999
+
888
1000
  async function updateConfig(harness) {
889
1001
  const config = readConfig(harness);
890
1002
  const savedPolicy = normalizedPolicy(config);
@@ -1059,7 +1171,24 @@ async function postWorkFile(config, file, contents) {
1059
1171
  function validWorkFileJob(receipt) {
1060
1172
  return typeof receipt?.id === "string" && receipt.id.length > 0 &&
1061
1173
  (ACTIVE_WORK_FILE_STATUSES.has(receipt.status) || FINAL_WORK_FILE_STATUSES.has(receipt.status) || receipt.status === "partial") &&
1062
- Number.isInteger(receipt.claimsCreated) && receipt.claimsCreated >= 0;
1174
+ Number.isInteger(receipt.claimsCreated) && receipt.claimsCreated >= 0 &&
1175
+ (receipt.candidateCount === undefined || (Number.isInteger(receipt.candidateCount) && receipt.candidateCount >= 0));
1176
+ }
1177
+
1178
+ function workFileResult(job) {
1179
+ const claims = `${job.claimsCreated} claim${job.claimsCreated === 1 ? "" : "s"}`;
1180
+ const candidates = job.candidateCount > 0 ? `, ${job.candidateCount} needs-evidence suggestion${job.candidateCount === 1 ? "" : "s"}` : "";
1181
+ return `${job.status} (${claims}${candidates})`;
1182
+ }
1183
+
1184
+ function workFileEntries(state, folder) {
1185
+ if (state.workFiles?.folder !== folder) return {};
1186
+ const files = state.workFiles.files || {};
1187
+ if (state.workFiles.version === WORK_FILE_SYNC_VERSION) return files;
1188
+ // Replay old completed checkpoints once, but never abandon an accepted job.
1189
+ return Object.fromEntries(Object.entries(files)
1190
+ .filter(([, entry]) => entry && typeof entry === "object" && entry.jobId)
1191
+ .map(([name, entry]) => [name, { ...entry, replayOnCompletion: true }]));
1063
1192
  }
1064
1193
 
1065
1194
  async function getWorkFileJob(config, jobId) {
@@ -1090,7 +1219,7 @@ async function getWorkFileJob(config, jobId) {
1090
1219
 
1091
1220
  function saveWorkFileEntry(harness, folder, name, entry) {
1092
1221
  const state = readState(harness);
1093
- const files = state.workFiles?.folder === folder && state.workFiles.version === WORK_FILE_SYNC_VERSION ? state.workFiles.files || {} : {};
1222
+ const files = workFileEntries(state, folder);
1094
1223
  state.workFiles = { folder, version: WORK_FILE_SYNC_VERSION, files: { ...files, [name]: entry } };
1095
1224
  saveState(harness, state);
1096
1225
  }
@@ -1099,7 +1228,7 @@ async function syncWorkFiles(harness, config = readConfig(harness)) {
1099
1228
  if (typeof config.inputFolder !== "string") throw new Error("No input folder is configured. Run connect again to enable file sync.");
1100
1229
  const folder = validateInputFolder(configuredInputFolder(config));
1101
1230
  const state = readState(harness);
1102
- const prior = state.workFiles?.folder === folder && state.workFiles.version === WORK_FILE_SYNC_VERSION ? state.workFiles.files || {} : {};
1231
+ const prior = workFileEntries(state, folder);
1103
1232
  let submitted = 0;
1104
1233
  const failedFiles = new Set();
1105
1234
  const unverifiedJobs = new Set();
@@ -1108,9 +1237,14 @@ async function syncWorkFiles(harness, config = readConfig(harness)) {
1108
1237
  try {
1109
1238
  const job = await getWorkFileJob(config, entry.jobId);
1110
1239
  if (FINAL_WORK_FILE_STATUSES.has(job.status)) {
1111
- saveWorkFileEntry(harness, folder, name, entry.sha256);
1112
- prior[name] = entry.sha256;
1113
- console.error(`${name}: ${job.status} (${job.claimsCreated} claim${job.claimsCreated === 1 ? "" : "s"}).`);
1240
+ console.error(`${name}: ${workFileResult(job)}.`);
1241
+ if (entry.replayOnCompletion) {
1242
+ prior[name] = { ...entry, status: "partial" };
1243
+ saveWorkFileEntry(harness, folder, name, prior[name]);
1244
+ } else {
1245
+ saveWorkFileEntry(harness, folder, name, entry.sha256);
1246
+ prior[name] = entry.sha256;
1247
+ }
1114
1248
  } else {
1115
1249
  prior[name] = { ...entry, status: job.status };
1116
1250
  saveWorkFileEntry(harness, folder, name, prior[name]);
@@ -1136,7 +1270,7 @@ async function syncWorkFiles(harness, config = readConfig(harness)) {
1136
1270
  saveWorkFileEntry(harness, folder, name, entry);
1137
1271
  prior[name] = entry;
1138
1272
  submitted += 1;
1139
- console.error(`${name}: ${job.status}${FINAL_WORK_FILE_STATUSES.has(job.status) ? ` (${job.claimsCreated} claim${job.claimsCreated === 1 ? "" : "s"})` : " (processing in background)"}.`);
1273
+ console.error(`${name}: ${FINAL_WORK_FILE_STATUSES.has(job.status) ? workFileResult(job) : `${job.status} (processing in background)`}.`);
1140
1274
  } catch (error) {
1141
1275
  failedFiles.add(name);
1142
1276
  console.error(`${name} remains pending: ${error.message}`);
@@ -1383,7 +1517,7 @@ function status(harness) {
1383
1517
  const state = readState(harness);
1384
1518
  const pending = readJSON(pendingPath(harness), { segments: [] });
1385
1519
  const folder = config.inputFolder ? configuredInputFolder(config) : null;
1386
- const entries = state.workFiles?.version === WORK_FILE_SYNC_VERSION && state.workFiles.folder === folder ? Object.values(state.workFiles.files || {}) : [];
1520
+ const entries = folder ? Object.values(workFileEntries(state, folder)) : [];
1387
1521
  let unsupportedFiles = null;
1388
1522
  let inputFolderError = null;
1389
1523
  if (folder) {
@@ -1454,6 +1588,7 @@ async function main() {
1454
1588
  const harness = requireHarness(option("--harness"));
1455
1589
  if (command === "install") return install(harness);
1456
1590
  if (command === "connect") return connect(harness);
1591
+ if (command === "disconnect") return disconnect(harness);
1457
1592
  if (command === "config" && subCommand === "update") return updateConfig(harness);
1458
1593
  if (command === "sync") return sync(harness);
1459
1594
  if (command === "sync-files") return syncFilesNow(harness);
@@ -1463,7 +1598,7 @@ async function main() {
1463
1598
  if (command === "status") return status(harness);
1464
1599
  if (command === "sessions" || command === "discover" || (command === "session" && (subCommand === "discover" || subCommand === "list" || !subCommand || subCommand.startsWith("-")))) return sessions(harness);
1465
1600
  if (command === "review") return review(harness);
1466
- throw new Error("Use install, connect, config update, status, sessions, review, sync, sync-files, pending, approve, or scheduled-sync.");
1601
+ throw new Error("Use install, connect, disconnect, config update, status, sessions, review, sync, sync-files, pending, approve, or scheduled-sync.");
1467
1602
  }
1468
1603
 
1469
1604
  if (require.main === module) {
@@ -1475,4 +1610,4 @@ if (require.main === module) {
1475
1610
  });
1476
1611
  }
1477
1612
 
1478
- module.exports = { approvalNotificationInvocation, claudeReview, claudeSession, codexReview, codexSession, configPath, configuredSyncPolicy, defaultInputFolder, finalAssistantNote, install, installDestination, localSessions, normalizedPolicy, notificationSessionRef, opencodeQuery, opencodeReview, opencodeSessions, payloadForSegment, pendingSegments, prepareInputFolder, readSecret, removeHooks, requireSafeApiUrl, reviewCandidates, schedulerArtifact, statePath, syncWorkFiles, textIsSafe, unsyncedCodexSessions, validateInputFolder, validSyncReceipt, validateApprovalMode, validateIntervalHours, validateSync, workFiles };
1613
+ module.exports = { approvalNotificationInvocation, claudeReview, claudeSession, codexReview, codexSession, configPath, configuredSyncPolicy, connect, defaultInputFolder, disconnect, finalAssistantNote, install, installDestination, localSessions, normalizedPolicy, notificationSessionRef, opencodeQuery, opencodeReview, opencodeSessions, payloadForSegment, pendingSegments, prepareInputFolder, readSecret, removeHooks, requireSafeApiUrl, reviewCandidates, schedulerArtifact, statePath, syncWorkFiles, textIsSafe, unsyncedCodexSessions, validateConnection, validateInputFolder, validSyncReceipt, validateApprovalMode, validateIntervalHours, validateSync, workFiles };
@@ -2,9 +2,9 @@
2
2
  description: Inspect or sync redacted OpenCode activity using the saved Shifu policy.
3
3
  ---
4
4
 
5
- Run `npx @useshifu/coding-harness status --harness opencode` first.
5
+ Run `npx --yes @useshifu/coding-harness status --harness opencode` first.
6
6
 
7
- For an explicit Shifu input-folder sync, run `sync-files --harness opencode`. It polls unfinished jobs before submitting new or changed `.txt`, `.md`, `.rtf`, and `.docx` files up to 20 MB. Upload starts asynchronous server processing; check status on the next manual or scheduled sync. Only a final `processed`, `no_claims`, or `duplicate` result completes the local checkpoint; a `partial` result remains retryable. Unsupported files are counted but not uploaded. No claim-review step is needed. Do not treat another speaker's work or a meeting action item as the user's contribution. The server parses and sanitizes files; the local runner does not redact file contents.
7
+ For an explicit Shifu input-folder sync, run `sync-files --harness opencode`. It polls unfinished jobs before submitting new or changed `.txt`, `.md`, `.rtf`, and `.docx` files up to 20 MB. Upload starts asynchronous server processing; check status on the next manual or scheduled sync. Only a final `processed`, `no_claims`, or `duplicate` result completes the local checkpoint; a `partial` result remains retryable. Completed file checkpoints from the previous runner are replayed once; existing in-flight jobs are polled before replay. Report published claims and needs-evidence suggestions separately. `no_claims` may still include suggestions, which are not verified user contributions. Unsupported files are counted but not uploaded. No claim-review step is needed. Do not treat another speaker's work or a meeting action item as the user's contribution. The server parses and sanitizes files; the local runner does not redact file contents, and file text or excerpts must not appear in logs.
8
8
 
9
9
  If manual approval has queued scheduled work, run `pending --harness opencode`, show the complete redacted list, and ask once. After approval, run `approve --approved --harness opencode`.
10
10
 
@@ -12,8 +12,8 @@ For an explicit sync-now request:
12
12
 
13
13
  1. Run `sessions --harness opencode --hours 24`, using 48 or 72 only when requested.
14
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 generic `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 is valid when only the safe session summary remains.
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.
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, names, customer details, or proprietary identifiers.
18
18
 
19
- To connect or finish onboarding, run `connect --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>`).
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>`).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@useshifu/coding-harness",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "Scheduled, redacted coding-harness activity sync for Shifu",
5
5
  "bin": {
6
6
  "coding-harness": "bin/shifu-harness.js",
@@ -16,42 +16,44 @@ Only describe work attributable to the user. A team action item, meeting discuss
16
16
  When the user asks to connect, run:
17
17
 
18
18
  ```sh
19
- npx @useshifu/coding-harness connect --harness codex
19
+ npx --yes @useshifu/coding-harness connect --harness codex
20
20
  ```
21
21
 
22
- `connect` 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. If a connection already exists, `connect` keeps its key, checkpoints, and chosen policy. It also excludes the setup session when the harness exposes its current opaque session reference. No content is sent during setup.
22
+ `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.
23
+
24
+ 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.
23
25
 
24
26
  When the user asks to change the interval, approval mode, or input folder, run:
25
27
 
26
28
  ```sh
27
- npx @useshifu/coding-harness config update --harness codex --input-folder <existing-readable-folder>
29
+ npx --yes @useshifu/coding-harness config update --harness codex --input-folder <existing-readable-folder>
28
30
  ```
29
31
 
30
32
  Use only the flags the user wants to change; existing settings are preserved. The CLI validates a custom folder before saving. Do not add a second confirmation around either command.
31
33
 
32
34
  ## Inspect status or queued work
33
35
 
34
- Run `npx @useshifu/coding-harness status --harness codex` to inspect the saved policy, next scheduled run, pending count, and checkpoints.
36
+ Run `npx --yes @useshifu/coding-harness status --harness codex` to inspect the saved policy, next scheduled run, pending count, and checkpoints.
35
37
 
36
- For an explicit input-folder sync, run `npx @useshifu/coding-harness sync-files --harness codex`. It first polls previously accepted file jobs, then submits new or changed supported files. Uploading starts background processing; it does not wait for extraction. A later manual or scheduled sync polls status again. Only `processed`, `no_claims`, or `duplicate` advances a file's completed checkpoint; `partial` is retried on the next sync. No claim review or manual approval is needed for files. Report submitted, processing, retry-needed, and unsupported counts separately. Do not claim a submitted file was fully processed. If a file fails, leave it pending and report the error.
38
+ For an explicit input-folder sync, run `npx --yes @useshifu/coding-harness sync-files --harness codex`. It first polls previously accepted file jobs, then submits new or changed supported files. Uploading starts background processing; it does not wait for extraction. A later manual or scheduled sync polls status again. Only `processed`, `no_claims`, or `duplicate` advances a file's completed checkpoint; `partial` is retried on the next sync. The upgraded runner replays old completed checkpoints once, while preserving and polling in-flight jobs before their replay. No claim review or manual approval is needed for files. Report submitted, processing, retry-needed, unsupported, published-claim, and needs-evidence suggestion counts separately. Do not claim a submitted file was fully processed. If a file fails, leave it pending and report the error. Never print file text, candidate statements, or excerpts into logs.
37
39
 
38
- The folder runner accepts nonempty `.txt`, `.md`, `.rtf`, and `.docx` files up to 20 MB; the server accepts at most 500,000 extractable characters per file and splits that text into bounded chunks. Empty, malformed, binary, and renamed unsupported files cannot yield claims. A `no_claims` result means processing finished but no sufficiently grounded contribution could be attributed to the user; it does not mean upload failed. For a named transcript, confirm the user's Shifu Profile display name matches their speaker label before expecting automated attribution. Never promise a claim from every file or infer one from an action item. The upload has a transport deadline, but model processing has no upload-request timeout; failed chunks back off and can be retried without republishing completed chunks.
40
+ The folder runner accepts nonempty `.txt`, `.md`, `.rtf`, and `.docx` files up to 20 MB; the server accepts at most 500,000 extractable characters per file and splits that text into bounded chunks. Empty, malformed, binary, and renamed unsupported files cannot yield claims. A `no_claims` result means processing finished with no published claims; it may still have retained needs-evidence suggestions. Suggestions are possible contributions with unresolved evidence or attribution, not verified claims. If both counts are zero, no attributable contribution was extracted; upload itself may still have succeeded. For a named transcript, confirm the user's Shifu Profile display name matches their speaker label before expecting automated attribution. Never promise a claim from every file or infer one from an action item. The upload has a transport deadline, but model processing has no upload-request timeout; failed chunks back off and can be retried without republishing completed chunks.
39
41
 
40
42
  In manual mode, scheduled runs accumulate redacted segments locally. Run:
41
43
 
42
44
  ```sh
43
- npx @useshifu/coding-harness pending --harness codex
45
+ npx --yes @useshifu/coding-harness pending --harness codex
44
46
  ```
45
47
 
46
- Show the returned list and ask for one approval for the whole list. After approval, run `npx @useshifu/coding-harness approve --approved --harness codex`. Do not ask again.
48
+ Show the returned list and ask for one approval for the whole list. After approval, run `npx --yes @useshifu/coding-harness approve --approved --harness codex`. Do not ask again.
47
49
 
48
50
  ## Sync now
49
51
 
50
52
  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.
51
53
 
52
- 1. Run `npx @useshifu/coding-harness sessions --harness codex --hours <24|48|72>`.
54
+ 1. Run `npx --yes @useshifu/coding-harness sessions --harness codex --hours <24|48|72>`.
53
55
  2. Ignore `blockedSessions` unless the user specifically selects one. For a selected older backlog, add `--session-ref <sessionRef> --all`.
54
- 3. For each returned segment, run `npx @useshifu/coding-harness review --harness codex --session-ref <sessionRef> --hours <24|48|72>`.
56
+ 3. For each returned segment, run `npx --yes @useshifu/coding-harness review --harness codex --session-ref <sessionRef> --hours <24|48|72>`.
55
57
  4. Prepare a version-four payload from the returned final-assistant notes. Keep the returned `sessionRef`, `fromTurn`, and `toTurn` unchanged.
56
58
  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.
57
59
  6. Repeat discovery after accepted segments until the selected window is exhausted.
@@ -76,13 +78,13 @@ Use `kind: "activity"` for general harness work; it does not require a technical
76
78
  Automatic mode:
77
79
 
78
80
  ```sh
79
- printf '%s' '<redacted JSON>' | npx @useshifu/coding-harness sync --harness codex
81
+ printf '%s' '<redacted JSON>' | npx --yes @useshifu/coding-harness sync --harness codex
80
82
  ```
81
83
 
82
84
  Manual mode, only after the user's single approval:
83
85
 
84
86
  ```sh
85
- printf '%s' '<redacted JSON>' | npx @useshifu/coding-harness sync --approved --harness codex
87
+ printf '%s' '<redacted JSON>' | npx --yes @useshifu/coding-harness sync --approved --harness codex
86
88
  ```
87
89
 
88
90
  If a transport or server error remains after the runner's retries, report that the checkpoint was not advanced. Do not alter the segment or skip ahead.