@useshifu/coding-harness 0.3.0 → 0.3.1

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
@@ -1,6 +1,6 @@
1
1
  # Shifu coding-harness connector
2
2
 
3
- `@useshifu/coding-harness` connects Codex, Claude Code, or OpenCode to Shifu. It reads local final-assistant notes, never user prompts or raw transcripts, and sends incremental redacted activity through durable checkpoints. Unattended runs emit controlled use-case labels rather than copying source prose.
3
+ `@useshifu/coding-harness` connects Codex, Claude Code, or OpenCode to Shifu and watches one local input folder. Coding-session sync reads final-assistant notes, never user prompts or raw transcripts, and sends incremental redacted activity through durable checkpoints. Folder files are sent over TLS for server-side parsing, sanitization, and user-attribution checks; the local runner does not redact file contents.
4
4
 
5
5
  ## Connect
6
6
 
@@ -8,15 +8,22 @@
8
8
  npx @useshifu/coding-harness connect --harness <codex|claude_code|opencode>
9
9
  ```
10
10
 
11
- Connect installs the local instruction and scheduled runner, saves the connection key with owner-only permissions, and asks for two setup choices:
11
+ Connect installs the local instruction and scheduled runner, saves the connection key with owner-only permissions, and uses these defaults without setup questions:
12
12
 
13
- - sync interval: four hours by default, configurable from 0.25 to 720 hours;
14
- - approval mode: automatic by default, or manual for one approval covering the accumulated queue.
13
+ - sync interval: one hour, configurable from 0.25 to 720 hours;
14
+ - automatic coding-session sync (manual approval remains available for coding sessions);
15
+ - one input folder created at `~/Desktop/Shifu` on macOS, with a `README.md` explaining what it is for and shortcuts at `~/Downloads/Shifu` and `~/Documents/Shifu` when those parent folders exist.
15
16
 
16
- No session content is sent during setup. Re-running `connect` on an older installation keeps its key and checkpoints and completes the missing onboarding. Change the choices later with:
17
+ Place `.txt`, `.md`, `.rtf`, or `.docx` files directly in the input folder. Its `README.md` is never synced and an existing one is not overwritten. Screenshots and images are not processed. Existing files are picked up on the next scheduled run; unchanged files are not sent again. A failed file stays pending for retry. Folder files never require a claim-review step, even when coding-session sync is in manual mode. Shifu creates claims only when the file supports a specific contribution by the connected user; meeting action items or another person's work are not treated as the user's claims.
18
+
19
+ For named meeting transcripts, set your display name in Shifu Profile so your speaker turns can be identified. If the speaker cannot be matched, uncertain contributions are skipped.
20
+
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
+
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:
17
24
 
18
25
  ```sh
19
- npx @useshifu/coding-harness config update --harness <codex|claude_code|opencode>
26
+ npx @useshifu/coding-harness config update --harness <codex|claude_code|opencode> --interval-hours 1 --approval-mode automatic --input-folder ~/Desktop/Shifu
20
27
  ```
21
28
 
22
29
  Older configuration files remain on-demand and manual until either command completes onboarding; upgrading the package alone never enables background upload.
@@ -29,10 +36,13 @@ npx @useshifu/coding-harness sessions --harness codex --hours 24
29
36
  npx @useshifu/coding-harness review --harness codex --session-ref opaque-session-id
30
37
  npx @useshifu/coding-harness pending --harness codex
31
38
  npx @useshifu/coding-harness approve --harness codex
39
+ npx @useshifu/coding-harness sync-files --harness codex
32
40
  ```
33
41
 
34
42
  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.
35
43
 
44
+ The same scheduled run checks the input folder. `sync-files` checks it immediately without changing the schedule. It sends supported file bytes over the configured HTTPS connection (or localhost HTTP for development); the server keeps the raw bytes transient and returns a final processing receipt before the local file checkpoint advances. Files larger than 5 MB are left pending with an error. The scanner reads regular files directly in the chosen folder and ignores symlinks and subfolders.
45
+
36
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.
37
47
 
38
48
  ## Sync a prepared payload
@@ -35,11 +35,23 @@ const MAX_TITLE_LENGTH = 160;
35
35
  const MAX_WORK_DETAIL_LENGTH = 600;
36
36
  const CONFIG_VERSION = 2;
37
37
  const REDACTION_VERSION = 4;
38
- const DEFAULT_INTERVAL_HOURS = 4;
38
+ const DEFAULT_INTERVAL_HOURS = 1;
39
+ const LEGACY_INTERVAL_HOURS = 4;
39
40
  const MIN_INTERVAL_HOURS = 0.25;
40
41
  const MAX_INTERVAL_HOURS = 720;
41
42
  const SCHEDULER_TICK_MINUTES = 15;
42
43
  const MAX_RETRIES = 4;
44
+ const MAX_WORK_FILE_BYTES = 5 * 1024 * 1024;
45
+ const WORK_FILE_EXTENSIONS = new Set([".txt", ".md", ".rtf", ".docx"]);
46
+ const INPUT_FOLDER_README_NAME = "README.md";
47
+ const INPUT_FOLDER_README = `# Shifu sync folder
48
+
49
+ Put work logs, meeting transcripts, and other work documents directly in this folder. Shifu checks it every hour by default and keeps claims only when the file supports a contribution you made. Other people's work and meeting action items alone are not your claims.
50
+
51
+ Supported files: .txt, .md, .rtf, and .docx. Subfolders, links, images, and screenshots are not processed. This README is not synced.
52
+
53
+ 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.
54
+ `;
43
55
 
44
56
  function configRoot() {
45
57
  return process.env.SHIFU_CONFIG_ROOT || path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"), "shifu", "coding-harness");
@@ -204,7 +216,7 @@ function normalizedPolicy(config) {
204
216
  (approvalMode === "automatic" || approvalMode === "manual");
205
217
  return {
206
218
  onboardingComplete: configured,
207
- intervalHours: configured ? interval : DEFAULT_INTERVAL_HOURS,
219
+ intervalHours: configured ? interval : LEGACY_INTERVAL_HOURS,
208
220
  approvalMode: configured ? approvalMode : "manual",
209
221
  scheduleEnabled: configured ? config.syncPolicy.scheduleEnabled !== false : false,
210
222
  excludedSessionRefs: Array.isArray(config?.syncPolicy?.excludedSessionRefs) ? config.syncPolicy.excludedSessionRefs.filter(sessionRefIsSafe) : [],
@@ -252,6 +264,59 @@ function readState(harness) {
252
264
  return readJSON(statePath(harness), { sessions: {} });
253
265
  }
254
266
 
267
+ function defaultInputFolder(home = os.homedir()) {
268
+ return path.join(home, "Desktop", "Shifu");
269
+ }
270
+
271
+ function configuredInputFolder(config, selected = option("--input-folder")) {
272
+ const value = selected || config?.inputFolder || defaultInputFolder();
273
+ return path.resolve(value.startsWith("~/") ? path.join(os.homedir(), value.slice(2)) : value);
274
+ }
275
+
276
+ function validateInputFolder(folder) {
277
+ let stat;
278
+ try {
279
+ stat = fs.statSync(folder);
280
+ } catch {
281
+ throw new Error(`The Shifu input folder is not accessible: ${folder}`);
282
+ }
283
+ if (!stat.isDirectory()) throw new Error(`The Shifu input path is not a folder: ${folder}`);
284
+ try {
285
+ fs.accessSync(folder, fs.constants.R_OK | fs.constants.X_OK);
286
+ } catch {
287
+ throw new Error(`The Shifu input folder is not accessible: ${folder}`);
288
+ }
289
+ return folder;
290
+ }
291
+
292
+ function prepareInputFolder(folder, isDefault = false, platform = process.env.SHIFU_PLATFORM || process.platform, home = os.homedir()) {
293
+ if (isDefault) fs.mkdirSync(folder, { recursive: true, mode: 0o700 });
294
+ validateInputFolder(folder);
295
+ if (isDefault) {
296
+ try {
297
+ fs.writeFileSync(path.join(folder, INPUT_FOLDER_README_NAME), INPUT_FOLDER_README, { flag: "wx", mode: 0o600 });
298
+ } catch (error) {
299
+ if (error?.code !== "EEXIST") throw error;
300
+ }
301
+ }
302
+ const conflicts = [];
303
+ if (platform === "darwin") {
304
+ for (const location of ["Downloads", "Documents"]) {
305
+ const parent = path.join(home, location);
306
+ if (!fs.existsSync(parent)) continue;
307
+ const alias = path.join(parent, "Shifu");
308
+ try {
309
+ const existing = fs.lstatSync(alias);
310
+ if (!existing.isSymbolicLink() || path.resolve(parent, fs.readlinkSync(alias)) !== folder) conflicts.push(alias);
311
+ } catch (error) {
312
+ if (error?.code !== "ENOENT") throw error;
313
+ fs.symlinkSync(folder, alias, "dir");
314
+ }
315
+ }
316
+ }
317
+ return conflicts;
318
+ }
319
+
255
320
  function saveState(harness, state) {
256
321
  writeJSON(statePath(harness), state);
257
322
  }
@@ -595,17 +660,6 @@ async function confirm(question) {
595
660
  }
596
661
  }
597
662
 
598
- async function ask(question, fallback) {
599
- const terminal = prompt();
600
- try {
601
- const answer = (await terminal.question(`${question} [${fallback}] `)).trim();
602
- return answer || fallback;
603
- } finally {
604
- terminal.close();
605
- if (terminal.shifuInput !== process.stdin) terminal.shifuInput.destroy();
606
- }
607
- }
608
-
609
663
  async function readSecret(input = process.stdin) {
610
664
  const ownsInput = !input.isTTY;
611
665
  if (ownsInput) input = new tty.ReadStream(fs.openSync(process.platform === "win32" ? "CONIN$" : "/dev/tty", "r"));
@@ -799,13 +853,10 @@ function scheduleNextRun(harness, intervalHours) {
799
853
  async function chooseSyncPolicy(existing) {
800
854
  const intervalOption = option("--interval-hours");
801
855
  const approvalOption = option("--approval-mode");
802
- const intervalHours = validateIntervalHours(intervalOption ?? await ask("Sync every how many hours?", String(existing.intervalHours || DEFAULT_INTERVAL_HOURS)));
803
- let approvalMode = approvalOption;
804
- if (approvalMode === undefined) {
805
- const answer = (await ask("Approval mode: automatic or manual?", existing.approvalMode || "automatic")).toLowerCase();
806
- approvalMode = answer === "auto" ? "automatic" : answer;
807
- }
808
- return { intervalHours, approvalMode: validateApprovalMode(approvalMode) };
856
+ return {
857
+ intervalHours: validateIntervalHours(intervalOption ?? existing.intervalHours ?? DEFAULT_INTERVAL_HOURS),
858
+ approvalMode: validateApprovalMode(approvalOption ?? existing.approvalMode ?? "automatic"),
859
+ };
809
860
  }
810
861
 
811
862
  async function connect(harness) {
@@ -817,12 +868,16 @@ async function connect(harness) {
817
868
  const currentPolicy = savedPolicy?.onboardingComplete ? savedPolicy : { intervalHours: DEFAULT_INTERVAL_HOURS, approvalMode: "automatic", excludedSessionRefs: savedPolicy?.excludedSessionRefs || [] };
818
869
  const selected = await chooseSyncPolicy(currentPolicy);
819
870
  const syncPolicy = configuredSyncPolicy(existing || {}, harness, selected.intervalHours, selected.approvalMode);
871
+ const inputFolder = configuredInputFolder(existing);
872
+ const conflicts = prepareInputFolder(inputFolder, inputFolder === defaultInputFolder());
820
873
  install(harness, true);
821
874
  const scheduleFile = installSchedule(harness);
822
- writeJSON(configPath(harness), { ...existing, version: CONFIG_VERSION, apiUrl, token, harness, syncPolicy });
875
+ writeJSON(configPath(harness), { ...existing, version: CONFIG_VERSION, apiUrl, token, harness, syncPolicy, inputFolder });
823
876
  scheduleNextRun(harness, syncPolicy.intervalHours);
824
877
  console.error(`${HARNESS_NAMES[harness]} is connected. It will sync every ${syncPolicy.intervalHours} hours with ${syncPolicy.approvalMode} approval.`);
825
- console.error(`Schedule installed at ${scheduleFile}. No session content was sent during setup.`);
878
+ console.error(`Shifu sync folder: ${inputFolder}. Place .txt, .md, .rtf, or .docx files there. Only grounded contributions will become claims.`);
879
+ if (conflicts.length) console.error(`Existing Shifu shortcut(s) were left unchanged: ${conflicts.join(", ")}.`);
880
+ console.error(`Schedule installed at ${scheduleFile}. No content was sent during setup.`);
826
881
  }
827
882
 
828
883
  async function updateConfig(harness) {
@@ -830,13 +885,18 @@ async function updateConfig(harness) {
830
885
  const savedPolicy = normalizedPolicy(config);
831
886
  const selected = await chooseSyncPolicy(savedPolicy.onboardingComplete ? savedPolicy : { intervalHours: DEFAULT_INTERVAL_HOURS, approvalMode: "automatic" });
832
887
  const scheduleEnabled = option("--schedule") !== "off";
888
+ const inputFolder = configuredInputFolder(config);
889
+ const conflicts = prepareInputFolder(inputFolder, inputFolder === defaultInputFolder());
833
890
  config.version = CONFIG_VERSION;
834
891
  config.syncPolicy = configuredSyncPolicy(config, harness, selected.intervalHours, selected.approvalMode, scheduleEnabled);
892
+ config.inputFolder = inputFolder;
835
893
  install(harness, true);
836
894
  if (scheduleEnabled) installSchedule(harness);
837
895
  writeJSON(configPath(harness), config);
838
896
  scheduleNextRun(harness, config.syncPolicy.intervalHours);
839
897
  console.error(`${HARNESS_NAMES[harness]} now syncs every ${config.syncPolicy.intervalHours} hours with ${config.syncPolicy.approvalMode} approval${scheduleEnabled ? "" : "; scheduling is off"}.`);
898
+ console.error(`Input folder: ${inputFolder}.`);
899
+ if (conflicts.length) console.error(`Existing Shifu shortcut(s) were left unchanged: ${conflicts.join(", ")}.`);
840
900
  }
841
901
 
842
902
  function textIsSafe(value, maximum) {
@@ -929,6 +989,89 @@ async function postSync(config, input) {
929
989
  throw lastError;
930
990
  }
931
991
 
992
+ function workFiles(folder) {
993
+ return fs.readdirSync(validateInputFolder(folder), { withFileTypes: true })
994
+ .filter((entry) => entry.isFile() && entry.name !== INPUT_FOLDER_README_NAME && WORK_FILE_EXTENSIONS.has(path.extname(entry.name).toLowerCase()))
995
+ .map((entry) => path.join(folder, entry.name))
996
+ .sort();
997
+ }
998
+
999
+ function readWorkFile(file) {
1000
+ const descriptor = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
1001
+ try {
1002
+ const before = fs.fstatSync(descriptor);
1003
+ if (!before.isFile() || before.size > MAX_WORK_FILE_BYTES) throw new Error("Only regular files up to 5 MB are supported.");
1004
+ const bytes = fs.readFileSync(descriptor);
1005
+ const after = fs.fstatSync(descriptor);
1006
+ if (bytes.length > MAX_WORK_FILE_BYTES || before.size !== after.size || before.mtimeMs !== after.mtimeMs) {
1007
+ throw new Error("The file changed while being read; it will be retried later.");
1008
+ }
1009
+ return { bytes, modifiedAt: after.mtime.toISOString(), sha256: createHash("sha256").update(bytes).digest("hex") };
1010
+ } finally {
1011
+ fs.closeSync(descriptor);
1012
+ }
1013
+ }
1014
+
1015
+ async function postWorkFile(config, file, contents) {
1016
+ let lastError;
1017
+ for (let attempt = 0; attempt < MAX_RETRIES; attempt += 1) {
1018
+ let response;
1019
+ try {
1020
+ const form = new FormData();
1021
+ form.append("file", new Blob([contents.bytes]), path.basename(file));
1022
+ form.append("sourceModifiedAt", contents.modifiedAt);
1023
+ response = await fetch(`${config.apiUrl}/v1/connectors/work-files`, {
1024
+ method: "POST",
1025
+ headers: { Authorization: `Bearer ${config.token}` },
1026
+ body: form,
1027
+ signal: AbortSignal.timeout(15 * 60_000),
1028
+ });
1029
+ } catch (error) {
1030
+ lastError = new Error(`Could not send ${path.basename(file)} to Shifu: ${error?.cause?.code || error?.message || "unknown transport error"}.`);
1031
+ if (attempt + 1 < MAX_RETRIES) await sleep(retryDelay(undefined, attempt));
1032
+ continue;
1033
+ }
1034
+ const payload = await response.json().catch(() => undefined);
1035
+ if (response.ok) {
1036
+ const receipt = payload?.data;
1037
+ if (["processed", "no_claims", "duplicate"].includes(receipt?.status) && Number.isInteger(receipt?.claimsCreated) && receipt.claimsCreated >= 0) return receipt;
1038
+ throw new Error(`Shifu returned an invalid receipt for ${path.basename(file)}. Local state was not advanced.`);
1039
+ }
1040
+ lastError = new Error(payload?.error?.message || `Shifu rejected ${path.basename(file)} (${response.status}).`);
1041
+ if (response.status !== 429 && response.status < 500) throw lastError;
1042
+ if (attempt + 1 < MAX_RETRIES) await sleep(retryDelay(response, attempt));
1043
+ }
1044
+ throw lastError;
1045
+ }
1046
+
1047
+ async function syncWorkFiles(harness, config = readConfig(harness)) {
1048
+ if (typeof config.inputFolder !== "string") throw new Error("No input folder is configured. Run connect again to enable file sync.");
1049
+ const folder = validateInputFolder(configuredInputFolder(config));
1050
+ const state = readState(harness);
1051
+ const prior = state.workFiles?.folder === folder ? state.workFiles.files || {} : {};
1052
+ let synced = 0;
1053
+ let failures = 0;
1054
+ for (const file of workFiles(folder)) {
1055
+ const name = path.basename(file);
1056
+ try {
1057
+ const contents = readWorkFile(file);
1058
+ if (prior[name] === contents.sha256) continue;
1059
+ const receipt = await postWorkFile(config, file, contents);
1060
+ const latest = readState(harness);
1061
+ const files = latest.workFiles?.folder === folder ? latest.workFiles.files || {} : {};
1062
+ latest.workFiles = { folder, files: { ...files, [name]: contents.sha256 } };
1063
+ saveState(harness, latest);
1064
+ prior[name] = contents.sha256;
1065
+ synced += 1;
1066
+ console.error(`${name}: ${receipt.status} (${receipt.claimsCreated} claim${receipt.claimsCreated === 1 ? "" : "s"}).`);
1067
+ } catch (error) {
1068
+ failures += 1;
1069
+ console.error(`${name} remains pending: ${error.message}`);
1070
+ }
1071
+ }
1072
+ return { synced, failures };
1073
+ }
1074
+
932
1075
  async function sendSync(harness, input, config = readConfig(harness)) {
933
1076
  validateSync(input, harness);
934
1077
  const state = readState(harness);
@@ -949,6 +1092,7 @@ async function withSyncLock(harness, action) {
949
1092
  fs.mkdirSync(configRoot(), { recursive: true, mode: 0o700 });
950
1093
  const file = lockPath(harness);
951
1094
  let descriptor;
1095
+ let heartbeat;
952
1096
  try {
953
1097
  try {
954
1098
  descriptor = fs.openSync(file, "wx", 0o600);
@@ -960,8 +1104,13 @@ async function withSyncLock(harness, action) {
960
1104
  descriptor = fs.openSync(file, "wx", 0o600);
961
1105
  }
962
1106
  fs.writeFileSync(descriptor, `${process.pid}\n`);
1107
+ heartbeat = setInterval(() => {
1108
+ try { fs.futimesSync(descriptor, new Date(), new Date()); } catch {}
1109
+ }, 60_000);
1110
+ heartbeat.unref();
963
1111
  return await action();
964
1112
  } finally {
1113
+ if (heartbeat) clearInterval(heartbeat);
965
1114
  if (descriptor !== undefined) {
966
1115
  fs.closeSync(descriptor);
967
1116
  try { fs.unlinkSync(file); } catch {}
@@ -1073,6 +1222,18 @@ async function scheduledSync(harness) {
1073
1222
  const startedAt = Date.now();
1074
1223
  let failures = 0;
1075
1224
  let synced = 0;
1225
+ let syncedFiles = 0;
1226
+ let failedFiles = 0;
1227
+ if (config.inputFolder) {
1228
+ try {
1229
+ const files = await syncWorkFiles(harness, config);
1230
+ failedFiles = files.failures;
1231
+ syncedFiles = files.synced;
1232
+ } catch (error) {
1233
+ failedFiles = 1;
1234
+ console.error(`Shifu input folder remains pending: ${error.message}`);
1235
+ }
1236
+ }
1076
1237
  if (policy.approvalMode === "manual") {
1077
1238
  const queue = rebuildPendingQueue(harness);
1078
1239
  const pendingFingerprint = createHash("sha256").update(queue.segments.map((segment) => `${segment.sessionRef}:${segment.fromTurn}:${segment.toTurn}`).join("\n")).digest("hex");
@@ -1093,12 +1254,14 @@ async function scheduledSync(harness) {
1093
1254
  state.schedule = {
1094
1255
  ...state.schedule,
1095
1256
  lastAttemptAt: startedAt,
1096
- lastResult: notificationError ? "awaiting_approval_notification_failed" : "awaiting_approval",
1257
+ lastResult: notificationError ? "awaiting_approval_notification_failed" : failedFiles ? "partial_failure" : "awaiting_approval",
1258
+ syncedFiles,
1259
+ failedFiles,
1097
1260
  pendingSegments: queue.segments.length,
1098
1261
  pendingFingerprint,
1099
1262
  approvalSessionRef,
1100
1263
  notificationError,
1101
- nextRunAt: startedAt + (notificationError ? SCHEDULER_TICK_MINUTES / 60 : policy.intervalHours) * 3_600_000,
1264
+ nextRunAt: startedAt + (notificationError || failedFiles ? SCHEDULER_TICK_MINUTES / 60 : policy.intervalHours) * 3_600_000,
1102
1265
  };
1103
1266
  } else {
1104
1267
  const blockedSessions = new Set();
@@ -1115,10 +1278,12 @@ async function scheduledSync(harness) {
1115
1278
  }
1116
1279
  state.schedule = {
1117
1280
  lastAttemptAt: startedAt,
1118
- lastResult: failures ? "partial_failure" : "complete",
1281
+ lastResult: failures || failedFiles ? "partial_failure" : "complete",
1119
1282
  syncedSegments: synced,
1120
1283
  failedSessions: failures,
1121
- nextRunAt: startedAt + (failures ? SCHEDULER_TICK_MINUTES / 60 : policy.intervalHours) * 3_600_000,
1284
+ syncedFiles,
1285
+ failedFiles,
1286
+ nextRunAt: startedAt + (failures || failedFiles ? SCHEDULER_TICK_MINUTES / 60 : policy.intervalHours) * 3_600_000,
1122
1287
  };
1123
1288
  }
1124
1289
  const latest = readState(harness);
@@ -1132,7 +1297,14 @@ function status(harness) {
1132
1297
  const config = readConfig(harness);
1133
1298
  const state = readState(harness);
1134
1299
  const pending = readJSON(pendingPath(harness), { segments: [] });
1135
- console.log(JSON.stringify({ harness, apiUrl: config.apiUrl, syncPolicy: normalizedPolicy(config), schedule: state.schedule || null, pendingSegments: pending.segments.length, sessions: state.sessions }, null, 2));
1300
+ console.log(JSON.stringify({ harness, apiUrl: config.apiUrl, inputFolder: config.inputFolder || null, syncPolicy: normalizedPolicy(config), schedule: state.schedule || null, pendingSegments: pending.segments.length, syncedFiles: Object.keys(state.workFiles?.files || {}).length, sessions: state.sessions }, null, 2));
1301
+ }
1302
+
1303
+ async function syncFilesNow(harness) {
1304
+ const result = await withSyncLock(harness, () => syncWorkFiles(harness));
1305
+ if (result?.skipped) throw new Error("Another Shifu sync is already running. Files remain pending.");
1306
+ console.error(`Checked the input folder: ${result.synced} sent, ${result.failures} pending.`);
1307
+ if (result.failures) throw new Error("Some input files remain pending. Retry sync-files after resolving the errors above.");
1136
1308
  }
1137
1309
 
1138
1310
  function sessions(harness) {
@@ -1192,13 +1364,14 @@ async function main() {
1192
1364
  if (command === "connect") return connect(harness);
1193
1365
  if (command === "config" && subCommand === "update") return updateConfig(harness);
1194
1366
  if (command === "sync") return sync(harness);
1367
+ if (command === "sync-files") return syncFilesNow(harness);
1195
1368
  if (command === "scheduled-sync") return scheduledSync(harness);
1196
1369
  if (command === "pending") return showPending(harness);
1197
1370
  if (command === "approve") return approvePending(harness);
1198
1371
  if (command === "status") return status(harness);
1199
1372
  if (command === "sessions" || command === "discover" || (command === "session" && (subCommand === "discover" || subCommand === "list" || !subCommand || subCommand.startsWith("-")))) return sessions(harness);
1200
1373
  if (command === "review") return review(harness);
1201
- throw new Error("Use install, connect, config update, status, sessions, review, sync, pending, approve, or scheduled-sync.");
1374
+ throw new Error("Use install, connect, config update, status, sessions, review, sync, sync-files, pending, approve, or scheduled-sync.");
1202
1375
  }
1203
1376
 
1204
1377
  if (require.main === module) {
@@ -1210,4 +1383,4 @@ if (require.main === module) {
1210
1383
  });
1211
1384
  }
1212
1385
 
1213
- module.exports = { approvalNotificationInvocation, claudeReview, claudeSession, codexReview, codexSession, configPath, configuredSyncPolicy, finalAssistantNote, install, installDestination, localSessions, normalizedPolicy, notificationSessionRef, opencodeQuery, opencodeReview, opencodeSessions, payloadForSegment, pendingSegments, readSecret, removeHooks, requireSafeApiUrl, reviewCandidates, schedulerArtifact, statePath, textIsSafe, unsyncedCodexSessions, validSyncReceipt, validateApprovalMode, validateIntervalHours, validateSync };
1386
+ 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 };
@@ -4,6 +4,8 @@ description: Inspect or sync redacted OpenCode activity using the saved Shifu po
4
4
 
5
5
  Run `npx @useshifu/coding-harness status --harness opencode` first.
6
6
 
7
+ For an explicit Shifu input-folder sync, run `sync-files --harness opencode`. It processes supported `.txt`, `.md`, `.rtf`, and `.docx` files in the configured folder without a claim-review step. 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.
8
+
7
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`.
8
10
 
9
11
  For an explicit sync-now request:
@@ -14,4 +16,4 @@ For an explicit sync-now request:
14
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`.
15
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.
16
18
 
17
- To connect or finish onboarding, run `connect --harness opencode`. To change the interval or approval policy later, run `config update --harness opencode`.
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>`).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@useshifu/coding-harness",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Scheduled, redacted coding-harness activity sync for Shifu",
5
5
  "bin": {
6
6
  "coding-harness": "bin/shifu-harness.js",
@@ -1,13 +1,15 @@
1
1
  ---
2
2
  name: shifu-sync
3
- description: Connect, configure, inspect, or sync redacted Codex activity to Shifu, including scheduled and manual-approval modes. Use when the user asks to connect Shifu, change sync settings, inspect pending activity, approve a queued sync, or sync recent harness work.
3
+ description: Connect, configure, inspect, or sync coding-harness activity and local work files to Shifu. Use when the user asks to connect Shifu, change sync settings, inspect pending activity, approve a queued coding sync, or sync recent harness work or input-folder files.
4
4
  ---
5
5
 
6
6
  # Shifu sync
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 upload user prompts, raw transcripts, tool output, source code, commands, credentials, URLs, file paths, names, customer details, or proprietary identifiers. The local runner reads final assistant notes only and enforces redaction and checkpoint rules.
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 only and enforces redaction and checkpoint rules. 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
+
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.
11
13
 
12
14
  ## Connect or reconfigure
13
15
 
@@ -17,20 +19,22 @@ When the user asks to connect, run:
17
19
  npx @useshifu/coding-harness connect --harness codex
18
20
  ```
19
21
 
20
- `connect` installs the current runner, asks once for the sync interval and approval mode, and installs a local schedule. The defaults are every four hours and automatic approval. If a connection already exists, `connect` keeps its key and checkpoints and completes onboarding only. It also excludes the setup session when the harness exposes its current opaque session reference. No session content is sent during setup.
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` directly inside the folder; images and screenshots are not supported. 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.
21
23
 
22
- When the user asks to change the interval or approval mode, run:
24
+ When the user asks to change the interval, approval mode, or input folder, run:
23
25
 
24
26
  ```sh
25
- npx @useshifu/coding-harness config update --harness codex
27
+ npx @useshifu/coding-harness config update --harness codex --input-folder <existing-readable-folder>
26
28
  ```
27
29
 
28
- Do not add a second confirmation around either command. The CLI owns onboarding input.
30
+ 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.
29
31
 
30
32
  ## Inspect status or queued work
31
33
 
32
34
  Run `npx @useshifu/coding-harness status --harness codex` to inspect the saved policy, next scheduled run, pending count, and checkpoints.
33
35
 
36
+ For an explicit input-folder sync, run `npx @useshifu/coding-harness sync-files --harness codex`. It sends supported files automatically and advances each local checkpoint only after a final server receipt. No claim review or manual approval is needed for files. If a file fails, leave it pending and report the error; do not claim it was synced.
37
+
34
38
  In manual mode, scheduled runs accumulate redacted segments locally. Run:
35
39
 
36
40
  ```sh