@useshifu/coding-harness 0.3.8 → 0.3.10

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
@@ -70,3 +70,9 @@ AI Profile observations are created automatically with each coding-session activ
70
70
  The category is one of `system_design`, `detailed_design`, `implementation`, `debugging_testing`, `production_support`, `research_analysis`, `product_planning`, `writing_communication`, `marketing_growth`, `learning`, or `other`. Evidence dimensions are `task_selection`, `goal_clarity`, `context_sharing`, `constraint_setting`, `work_decomposition`, `iterative_steering`, `critical_judgment`, `verification`, `application`, and `responsible_use`. Automatic observations cite the relevant turn and never infer a limiting behavior from silence. Unsupported dimensions remain unscored.
71
71
 
72
72
  The AI Profile page shows detected categories, model and thinking labels, and Jev scores supported by these limited observations. Local signals do not prove work quality or establish that another model setting would have performed better. Identical retries are safe.
73
+
74
+ ## Recommended package updates
75
+
76
+ The connection endpoint returns `recommendedPackageVersion`, currently pinned to the latest published release, `0.3.9`. Before `sync`, `sync-files`, `approve`, or `scheduled-sync`, the runner compares its embedded release version with that property. An older runner installs the exact recommended release through npm and resumes the original command with the updated runner. Equal or newer runners continue unchanged. Explicitly recommended major releases follow the same comparison rule. Missing or invalid recommendations, connection failures, and npm failures do not block sync. The connection, pending work, checkpoints, and schedule are preserved.
77
+
78
+ This update mechanism first ships in `0.3.10`. Runners from earlier releases need a one-time installation of a release containing it (`npx --yes @useshifu/coding-harness@0.3.10 install --harness codex`, after publication). Raise the server recommendation only after the target release is published.
@@ -4,7 +4,9 @@ const fs = require("node:fs");
4
4
  const { createHash } = require("node:crypto");
5
5
  const os = require("node:os");
6
6
  const path = require("node:path");
7
- const { execFileSync } = require("node:child_process");
7
+ const { execFile, execFileSync, spawn } = require("node:child_process");
8
+ const { promisify } = require("node:util");
9
+ const execFileAsync = promisify(execFile);
8
10
  const readline = require("node:readline/promises");
9
11
 
10
12
  const DEFAULT_API_URL = "https://api.useshifu.com";
@@ -37,7 +39,7 @@ const MAX_TURNS_PER_SEGMENT = 12;
37
39
  const MAX_TITLE_LENGTH = 160;
38
40
  const MAX_WORK_DETAIL_LENGTH = 600;
39
41
  const CONFIG_VERSION = 2;
40
- const RUNNER_VERSION = "0.3.8";
42
+ const RUNNER_VERSION = "0.3.10";
41
43
  const REDACTION_VERSION = 4;
42
44
  const DEFAULT_INTERVAL_HOURS = 1;
43
45
  const LEGACY_INTERVAL_HOURS = 4;
@@ -562,6 +564,7 @@ const WORK_ACTIONS = [
562
564
  { key: "reviewed", title: "Reviewed", pattern: /^(?:I\s+|We\s+)?(?:reviewed|audited)\b/i },
563
565
  { key: "investigated", title: "Investigated", pattern: /^(?:I\s+|We\s+)?(?:investigated|researched|evaluated|compared)\b/i },
564
566
  { key: "deployed", title: "Deployed", pattern: /^(?:I\s+|We\s+)?(?:deployed|published)\b/i },
567
+ { key: "decided", title: "Decided", pattern: /^(?:I\s+|We\s+)?(?:decided|chose|selected)\b/i },
565
568
  ];
566
569
 
567
570
  const WORK_OBJECTS = [
@@ -572,6 +575,11 @@ const WORK_OBJECTS = [
572
575
  { key: "claim_extraction", title: "claim extraction", pattern: /\b(?:claim extraction|claim generation|claim matching)\b/i },
573
576
  { key: "retry", title: "retry handling", pattern: /\b(?:retries|retry)\b/i },
574
577
  { key: "cache", title: "cache behavior", pattern: /\b(?:cache|caching|ttl)\b/i },
578
+ { key: "outbox", title: "transactional outbox", pattern: /\b(?:transactional outbox|outbox pattern)\b/i },
579
+ { key: "token_bucket", title: "token bucket limiter", pattern: /\btoken bucket limiter\b/i },
580
+ { key: "actor_scheduler", title: "actor scheduler", pattern: /\bactor scheduler\b/i },
581
+ { key: "quota_balancing", title: "quota balancing", pattern: /\bquota balancing\b/i },
582
+ { key: "shard_rebalancing", title: "shard rebalancing", pattern: /\bshard rebalancing\b/i },
575
583
  { key: "navigation", title: "navigation", pattern: /\b(?:navigation|sidebar|sub.?nav)\b/i },
576
584
  { key: "sync", title: "sync behavior", pattern: /\b(?:sync|synchronization|checkpoint)\b/i },
577
585
  { key: "auth", title: "authentication", pattern: /\b(?:authentication|sign.?in|login)\b/i },
@@ -626,6 +634,39 @@ const WORK_DETAILS = [
626
634
  { title: "mobile layout", pattern: /\bmobile layout\b/i },
627
635
  ];
628
636
 
637
+ // These terms are safe to retain without copying surrounding private prose.
638
+ const TECHNICAL_DETAILS = [
639
+ { title: "Redis", pattern: /\bRedis\b/i },
640
+ { title: "Kafka", pattern: /\bKafka\b/i },
641
+ { title: "Postgres", pattern: /\bPostgres(?:ql)?\b/i },
642
+ { title: "MongoDB", pattern: /\bMongoDB\b/i },
643
+ { title: "React", pattern: /\bReact\b/i },
644
+ { title: "Node.js", pattern: /\bNode\.js\b/i },
645
+ { title: "TypeScript", pattern: /\bTypeScript\b/i },
646
+ { title: "Python", pattern: /\bPython\b/i },
647
+ { title: "Spring Boot", pattern: /\bSpring Boot\b/i },
648
+ { title: "Kubernetes", pattern: /\bKubernetes\b/i },
649
+ { title: "Docker", pattern: /\bDocker\b/i },
650
+ { title: "GraphQL", pattern: /\bGraphQL\b/i },
651
+ { title: "gRPC", pattern: /\bgRPC\b/i },
652
+ { title: "cache-aside", pattern: /\bcache.aside\b/i },
653
+ { title: "transactional outbox", pattern: /\btransactional outbox\b/i },
654
+ { title: "circuit breaker", pattern: /\bcircuit breaker\b/i },
655
+ { title: "Builder Pattern", pattern: /\bBuilder Pattern\b/i },
656
+ { title: "event sourcing", pattern: /\bevent sourcing\b/i },
657
+ { title: "CQRS", pattern: /\bCQRS\b/i },
658
+ { title: "Saga Pattern", pattern: /\bSaga Pattern\b/i },
659
+ { title: "Repository Pattern", pattern: /\bRepository Pattern\b/i },
660
+ { title: "Dependency Injection", pattern: /\bDependency Injection\b/i },
661
+ { title: "OpenTelemetry", pattern: /\bOpenTelemetry\b/i },
662
+ { title: "Temporal", pattern: /\bTemporal\b/i },
663
+ { title: "Rust", pattern: /\bRust\b/i },
664
+ { title: "Tokio", pattern: /\bTokio\b/i },
665
+ { title: "consistent hashing", pattern: /\bconsistent hashing\b/i },
666
+ { title: "virtual nodes", pattern: /\bvirtual nodes\b/i },
667
+ { title: "adaptive jitter", pattern: /\badaptive jitter\b/i },
668
+ ];
669
+
629
670
  function controlledContexts(text) {
630
671
  const matches = WORK_CONTEXTS.flatMap((item) => {
631
672
  const match = item.pattern.exec(text);
@@ -640,15 +681,27 @@ function controlledContexts(text) {
640
681
  }
641
682
 
642
683
  function controlledDetails(text) {
643
- return WORK_DETAILS.map((item) => ({ item, index: item.pattern.exec(text)?.index }))
684
+ const known = [...WORK_DETAILS, ...TECHNICAL_DETAILS];
685
+ const ttl = /\b(\d{1,3})[ -]hour TTL\b/i.exec(text);
686
+ const latency = /\bp(?:50|90|95|99) latency from (\d{1,4}) ms to (\d{1,4}) ms\b/i.exec(text);
687
+ const reductionPattern = /\b(?:cutting|cut|reducing|reduced|decreasing|decreased|lowering|lowered)\s+(queue time|memory(?: usage)?)\s+by\s*(\d{1,3}(?:\.\d{1,2})?)%(?![\d.])/i;
688
+ const reduction = reductionPattern.exec(text);
689
+ if (ttl) known.push({ title: `${ttl[1]}-hour TTL`, pattern: /\b\d{1,3}[ -]hour TTL\b/i });
690
+ if (latency) known.push({ title: latency[0], pattern: /\bp(?:50|90|95|99) latency from \d{1,4} ms to \d{1,4} ms\b/i });
691
+ if (reduction && Number(reduction[2]) <= 100) {
692
+ const metric = reduction[1].toLowerCase().startsWith("memory") ? "memory usage" : "queue time";
693
+ known.push({ title: `${metric} reduced by ${reduction[2]}%`, pattern: reductionPattern });
694
+ }
695
+ return known.map((item) => ({ item, index: item.pattern.exec(text)?.index }))
644
696
  .filter(({ index }) => index !== undefined)
645
697
  .sort((left, right) => left.index - right.index)
646
- .slice(0, 3)
647
- .map(({ item }) => item.title);
698
+ .map(({ item }) => item.title)
699
+ .filter((detail, index, details) => details.indexOf(detail) === index)
700
+ .slice(0, 6);
648
701
  }
649
702
 
650
703
  function withoutNamedLabels(text) {
651
- return text.replace(/\b(?:Project|Client|Customer|Company)\s+[A-Z][A-Za-z0-9_-]*/g, "");
704
+ return text.replace(/\b(?:Project|Client|Customer|Company)\s+[A-Z][A-Za-z0-9_-]*/gi, "");
652
705
  }
653
706
 
654
707
  function safeActivityEvidence(notes) {
@@ -666,7 +719,7 @@ function safeActivityEvidence(notes) {
666
719
  sectionContexts = /\b(?:project|feature|flow|service|system|product)\b/i.test(label) ? controlledContexts(label) || [] : [];
667
720
  continue;
668
721
  }
669
- const line = rawLine.trim().replace(/^[-*]\s+/, "").split(/[.;—]/, 1)[0];
722
+ const line = rawLine.trim().replace(/^[-*]\s+/, "").split(/[;—]|[.!?](?=\s|$)/, 1)[0];
670
723
  const clauses = line.split(/\s+(?:and|but|while)\s+/i);
671
724
  const nextAction = clauses.findIndex((clause, index) => index > 0 && WORK_ACTIONS.some((item) => item.pattern.test(clause)));
672
725
  const workClause = withoutNamedLabels(nextAction > 0 ? clauses.slice(0, nextAction).join(" and ") : line);
@@ -710,7 +763,7 @@ function payloadForSegment(harness, segment) {
710
763
  summary,
711
764
  evidence,
712
765
  verification: [],
713
- decisions: [],
766
+ decisions: evidence.filter((item) => item.title.startsWith("Decided ")).slice(0, MAX_SYNC_ITEMS).map((item) => item.statement),
714
767
  redactionVersion: REDACTION_VERSION,
715
768
  };
716
769
  }
@@ -940,8 +993,14 @@ async function readSecret(input = process.stdin, output = process.stderr) {
940
993
 
941
994
  function copyRunner() {
942
995
  fs.mkdirSync(configRoot(), { recursive: true, mode: 0o700 });
943
- fs.copyFileSync(__filename, installedRunnerPath());
944
- fs.chmodSync(installedRunnerPath(), 0o700);
996
+ const temporary = `${installedRunnerPath()}.${process.pid}.tmp`;
997
+ try {
998
+ fs.copyFileSync(__filename, temporary);
999
+ fs.chmodSync(temporary, 0o700);
1000
+ fs.renameSync(temporary, installedRunnerPath());
1001
+ } finally {
1002
+ fs.rmSync(temporary, { force: true });
1003
+ }
945
1004
  }
946
1005
 
947
1006
  function copyDirectory(source, destination) {
@@ -1160,6 +1219,7 @@ async function validateConnection(apiUrl, token, harness) {
1160
1219
  if (!response.ok) throw new Error(`Shifu could not verify the connection key (HTTP ${response.status}). Nothing was changed.`);
1161
1220
  const result = await response.json().catch(() => null);
1162
1221
  if (result?.data?.connected !== true) throw new Error("Shifu returned an invalid connection check. Nothing was changed.");
1222
+ return result.data;
1163
1223
  }
1164
1224
 
1165
1225
  async function connect(harness, readKey = readSecret) {
@@ -1838,11 +1898,54 @@ function review(harness) {
1838
1898
  }, null, 2));
1839
1899
  }
1840
1900
 
1901
+ function olderPackageVersion(current, recommended) {
1902
+ const pattern = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
1903
+ if (!pattern.test(current) || !pattern.test(recommended)) return false;
1904
+ const left = current.split(".").map(BigInt);
1905
+ const right = recommended.split(".").map(BigInt);
1906
+ for (let index = 0; index < 3; index += 1) {
1907
+ if (left[index] !== right[index]) return left[index] < right[index];
1908
+ }
1909
+ return false;
1910
+ }
1911
+
1912
+ async function installPackageVersion(version, harness) {
1913
+ await execFileAsync("npm", ["exec", "--yes", `--package=@useshifu/coding-harness@${version}`, "--", "shifu-harness", "install", "--harness", harness, "--config-root", configRoot()], {
1914
+ timeout: 60_000,
1915
+ env: { ...process.env, PATH: `${process.env.PATH || ""}${path.delimiter}${path.dirname(process.execPath)}`, SHIFU_UPDATE_CHECKED: "1" },
1916
+ });
1917
+ }
1918
+
1919
+ async function updateRecommendedPackage(harness, { currentVersion = RUNNER_VERSION, fetchConnection = validateConnection, installVersion = installPackageVersion } = {}) {
1920
+ try {
1921
+ const config = readConfig(harness);
1922
+ const connection = await fetchConnection(config.apiUrl, config.token, harness);
1923
+ const recommended = connection?.recommendedPackageVersion;
1924
+ if (!olderPackageVersion(currentVersion, recommended)) return false;
1925
+ await installVersion(recommended, harness);
1926
+ console.error(`Updated Shifu coding harness to recommended version ${recommended}.`);
1927
+ return true;
1928
+ } catch {
1929
+ console.error("Could not check or install the recommended Shifu package. Continuing with the current runner.");
1930
+ return false;
1931
+ }
1932
+ }
1933
+
1841
1934
  async function main() {
1842
1935
  if (option("--config-root")) process.env.SHIFU_CONFIG_ROOT = path.resolve(option("--config-root"));
1843
1936
  const command = process.argv[2];
1844
1937
  const subCommand = process.argv[3];
1845
1938
  const harness = requireHarness(option("--harness"));
1939
+ if (["sync", "sync-files", "scheduled-sync", "approve"].includes(command) && process.env.SHIFU_UPDATE_CHECKED !== "1" && await updateRecommendedPackage(harness)) {
1940
+ const child = spawn(process.execPath, [installedRunnerPath(), ...process.argv.slice(2)], {
1941
+ env: { ...process.env, SHIFU_UPDATE_CHECKED: "1" }, stdio: "inherit",
1942
+ });
1943
+ await new Promise((resolve, reject) => {
1944
+ child.once("error", reject);
1945
+ child.once("exit", (code) => code === 0 ? resolve() : reject(new Error("The updated Shifu sync failed; pending work is preserved.")));
1946
+ });
1947
+ return;
1948
+ }
1846
1949
  if (command === "install") return install(harness);
1847
1950
  if (command === "connect") return connect(harness);
1848
1951
  if (command === "disconnect") return disconnect(harness);
@@ -1867,4 +1970,4 @@ if (require.main === module) {
1867
1970
  });
1868
1971
  }
1869
1972
 
1870
- module.exports = { approvalNotificationInvocation, claudeReview, claudeSession, codexReview, codexSession, configPath, configuredSyncPolicy, connect, defaultInputFolder, disconnect, finalAssistantNote, install, installDestination, localSessions, normalizedPolicy, notificationSessionRef, opencodeQuery, opencodeReview, opencodeSessions, payloadForSegment, pendingSegments, prepareInputFolder, profileForSegment, readSecret, removeHooks, requireSafeApiUrl, reviewCandidates, schedulerArtifact, statePath, syncWorkFiles, textIsSafe, unsyncedCodexSessions, validateAIProfileObservation, validateConnection, validateInputFolder, validSyncReceipt, validateApprovalMode, validateIntervalHours, validateSync, workFiles };
1973
+ module.exports = { olderPackageVersion, updateRecommendedPackage, installPackageVersion, approvalNotificationInvocation, claudeReview, claudeSession, codexReview, codexSession, configPath, configuredSyncPolicy, connect, defaultInputFolder, disconnect, finalAssistantNote, install, installDestination, localSessions, normalizedPolicy, notificationSessionRef, opencodeQuery, opencodeReview, opencodeSessions, payloadForSegment, pendingSegments, prepareInputFolder, profileForSegment, readSecret, removeHooks, requireSafeApiUrl, reviewCandidates, schedulerArtifact, statePath, syncWorkFiles, textIsSafe, unsyncedCodexSessions, validateAIProfileObservation, validateConnection, validateInputFolder, validSyncReceipt, validateApprovalMode, validateIntervalHours, validateSync, workFiles };
@@ -2,6 +2,8 @@
2
2
  description: Inspect or sync redacted OpenCode activity using the saved Shifu policy.
3
3
  ---
4
4
 
5
+ Before sync, the runner installs Shifu's exact `recommendedPackageVersion` only if its current version is older. Equal or newer versions are left alone. Update failures allow sync to continue; do not independently install npm latest.
6
+
5
7
  Run `npx --yes @useshifu/coding-harness status --harness opencode` first.
6
8
 
7
9
  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.
@@ -12,7 +14,7 @@ For an explicit sync-now request:
12
14
 
13
15
  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
16
  2. Review each returned segment with `review --harness opencode --session-ref <sessionRef> --hours <hours>`. For count-based selection, use `review --harness opencode --session-ref <sessionRef> --all` so older sessions are allowed.
15
- 3. Prepare a redaction-version-four payload from final assistant notes only. Use `activity` evidence for writing, feedback, research, planning, product work, and any work that is not clearly an engineering implementation or decision. Activity evidence has no technical scope. Preserve the supported action, feature or project purpose, specific problem, approach, and verified result. Synthesize a descriptive project label (such as "payment reconciliation service") from its purpose; never copy a raw label that could identify a company or person. Keep distinct contributions separate, including different fixes within one feature. An empty evidence list advances the checkpoint without creating a claim.
17
+ 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, technologies and design patterns used, 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
18
  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
19
  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
20
  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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@useshifu/coding-harness",
3
- "version": "0.3.8",
3
+ "version": "0.3.10",
4
4
  "description": "Scheduled, redacted coding-harness activity sync for Shifu",
5
5
  "bin": {
6
6
  "coding-harness": "bin/shifu-harness.js",
@@ -5,6 +5,8 @@ description: Connect, configure, inspect, or sync coding-harness activity, AI Pr
5
5
 
6
6
  # Shifu sync
7
7
 
8
+ Before manual or scheduled sync, the runner checks Shifu's `recommendedPackageVersion`. It automatically installs that exact release only when the installed version is older; equal or newer versions are left alone. Do not independently install npm latest. If the check or update fails, continue syncing with the current runner. Connection settings, pending work, checkpoints, and the schedule are preserved.
9
+
8
10
  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
11
 
10
12
  Never include user prompts, raw transcripts, tool output, source code, commands, credentials, URLs, file paths, company or person names, customer details, or proprietary identifiers in a coding-session payload. Synthesize descriptive project labels from their purpose rather than copying a raw name. The local coding runner reads final assistant notes for career activity and scans user turns locally for fixed AI Profile signals; it sends neither raw source. Files placed in the user's chosen Shifu input folder are a separate path: the runner sends supported files over TLS for server-side parsing and sanitization. Do not describe folder files as locally redacted.
@@ -64,7 +66,7 @@ For an explicit sync request, first read `status` to determine the saved approva
64
66
 
65
67
  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
68
 
67
- Make each supported item useful for later recall: retain the user's action, the feature or project purpose, the specific problem, the approach, and a verified result when the final note supports each part. A descriptive project label such as "payment reconciliation service" is useful; synthesize it from the work's purpose instead of copying a raw project label that might contain a company or person's name. Keep distinct contributions separate, including two fixes in one feature that addressed different problems. Omit names of companies and people, customer details, proprietary identifiers, paths, code snippets, and unsupported impact. If a detail cannot be safely described, keep the supported parts. Prefer a summary that names the main distinct work items over a generic count alone.
69
+ Make each supported item useful for later recall: retain the user's action, the feature or project purpose, the specific problem, the approach, and a verified result when the final note supports each part. Preserve supported technologies, design patterns, configuration choices, and measured results; these are useful work details, not identifiers to redact. A descriptive project label such as "payment reconciliation service" is useful; synthesize it from the work's purpose instead of copying a raw project label that might contain a company or person's name. Keep distinct contributions separate, including two fixes in one feature that addressed different problems. Omit names of companies and people, customer details, proprietary identifiers, paths, code snippets, credentials, and unsupported impact. If a detail cannot be safely described, keep the supported parts. Prefer a summary that names the main distinct work items over a generic count alone.
68
70
 
69
71
  ```json
70
72
  {