@useshifu/coding-harness 0.1.0
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 +20 -0
- package/bin/shifu-harness.js +301 -0
- package/commands/shifu-sync.md +11 -0
- package/package.json +23 -0
- package/skills/shifu-sync/SKILL.md +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Shifu coding-harness connector
|
|
2
|
+
|
|
3
|
+
`@useshifu/coding-harness` connects Codex, Claude Code, or OpenCode to Shifu through reviewed, incremental summaries. It never uploads raw transcripts or syncs in the background.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npx @useshifu/coding-harness install --harness codex
|
|
7
|
+
npx @useshifu/coding-harness connect --harness codex
|
|
8
|
+
npx @useshifu/coding-harness schedule --harness codex --after-turns 12 --after-hours 4
|
|
9
|
+
npx @useshifu/coding-harness activate --harness codex
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Replace `codex` with `claude_code` or `opencode` as needed. `connect`, `schedule`, `activate`, and `sync` each ask for confirmation. Activation writes only local harness configuration; it can mark a segment as due but cannot upload it. A due reminder appears when the harness next becomes active.
|
|
13
|
+
|
|
14
|
+
To submit a reviewed segment, pipe the exact payload to `sync`. The CLI prints it, asks again, and only then sends it to Shifu. Successful responses advance a local checkpoint; the server rejects out-of-order or changed retries.
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
printf '%s' '{"harness":"codex","sessionRef":"opaque-session-id","fromTurn":1,"toTurn":8,"summary":"Implemented a focused change and checked the result.","evidence":["Focused tests passed."],"decisions":[],"redactionVersion":1}' | npx @useshifu/coding-harness sync --harness codex
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The default API URL is `https://api.useshifu.com`. Override it during local or self-hosted use with `--api-url` on `connect`, or `SHIFU_API_URL`.
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const crypto = require("node:crypto");
|
|
4
|
+
const fs = require("node:fs");
|
|
5
|
+
const os = require("node:os");
|
|
6
|
+
const path = require("node:path");
|
|
7
|
+
const readline = require("node:readline/promises");
|
|
8
|
+
|
|
9
|
+
const DEFAULT_API_URL = "https://api.useshifu.com";
|
|
10
|
+
const HARNESS_NAMES = {
|
|
11
|
+
codex: "Codex",
|
|
12
|
+
claude_code: "Claude Code",
|
|
13
|
+
opencode: "OpenCode",
|
|
14
|
+
};
|
|
15
|
+
const TOKEN_PATTERN = /^cs_sk_[a-f0-9]{16}_[a-f0-9]{64}$/;
|
|
16
|
+
const SENSITIVE_PATTERNS = ["-----begin", "api_key", "authorization", "password=", "sk-", "ghp_", "@", "://", "`", "/"];
|
|
17
|
+
|
|
18
|
+
function configRoot() {
|
|
19
|
+
return path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"), "shifu", "coding-harness");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function configPath(harness) {
|
|
23
|
+
return path.join(configRoot(), `${harness}.json`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function statePath(harness) {
|
|
27
|
+
return path.join(configRoot(), `${harness}-checkpoints.json`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function installedRunnerPath() {
|
|
31
|
+
return path.join(configRoot(), "runner.js");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function requireHarness(value) {
|
|
35
|
+
if (!Object.hasOwn(HARNESS_NAMES, value)) throw new Error("Choose --harness codex, claude_code, or opencode.");
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function option(name) {
|
|
40
|
+
const index = process.argv.indexOf(name);
|
|
41
|
+
return index === -1 ? undefined : process.argv[index + 1];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function hasFlag(name) {
|
|
45
|
+
return process.argv.includes(name);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function readJSON(file, fallback) {
|
|
49
|
+
try {
|
|
50
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (error && error.code === "ENOENT") return fallback;
|
|
53
|
+
throw new Error(`Could not read ${file}.`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function writeJSON(file, value) {
|
|
58
|
+
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
59
|
+
const temporary = `${file}.${process.pid}.tmp`;
|
|
60
|
+
fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
|
|
61
|
+
fs.renameSync(temporary, file);
|
|
62
|
+
fs.chmodSync(file, 0o600);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function readConfig(harness) {
|
|
66
|
+
const config = readJSON(configPath(harness), undefined);
|
|
67
|
+
if (!config || typeof config.token !== "string" || typeof config.apiUrl !== "string") {
|
|
68
|
+
throw new Error(`No ${HARNESS_NAMES[harness]} connection is configured. Run connect first.`);
|
|
69
|
+
}
|
|
70
|
+
return config;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function readState(harness) {
|
|
74
|
+
return readJSON(statePath(harness), { sessions: {} });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function saveState(harness, state) {
|
|
78
|
+
writeJSON(statePath(harness), state);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function prompt() {
|
|
82
|
+
return readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function confirm(question) {
|
|
86
|
+
const terminal = prompt();
|
|
87
|
+
const answer = (await terminal.question(`${question} [y/N] `)).trim().toLowerCase();
|
|
88
|
+
terminal.close();
|
|
89
|
+
return answer === "y" || answer === "yes";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function copyRunner() {
|
|
93
|
+
fs.mkdirSync(configRoot(), { recursive: true, mode: 0o700 });
|
|
94
|
+
fs.copyFileSync(__filename, installedRunnerPath());
|
|
95
|
+
fs.chmodSync(installedRunnerPath(), 0o700);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function copyDirectory(source, destination) {
|
|
99
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true, mode: 0o700 });
|
|
100
|
+
fs.cpSync(source, destination, { recursive: true, force: true });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function shellCommand(harness, event) {
|
|
104
|
+
return `\"${process.execPath}\" \"${installedRunnerPath()}\" hook --harness ${harness} --event ${event}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function addHook(config, event, command, timeout) {
|
|
108
|
+
config.hooks ||= {};
|
|
109
|
+
config.hooks[event] ||= [];
|
|
110
|
+
const present = config.hooks[event].some((group) => group && Array.isArray(group.hooks) && group.hooks.some((hook) => hook.command === command));
|
|
111
|
+
if (!present) config.hooks[event].push({ hooks: [{ type: "command", command, timeout }] });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function enableCodex(harness) {
|
|
115
|
+
const file = path.join(os.homedir(), ".codex", "hooks.json");
|
|
116
|
+
const config = readJSON(file, { description: "Shifu reviewed sync reminders" });
|
|
117
|
+
addHook(config, "Stop", shellCommand(harness, "stop"), 3);
|
|
118
|
+
addHook(config, "SessionEnd", shellCommand(harness, "session_end"), 3);
|
|
119
|
+
writeJSON(file, config);
|
|
120
|
+
return file;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function enableClaude(harness) {
|
|
124
|
+
const file = path.join(os.homedir(), ".claude", "settings.json");
|
|
125
|
+
const config = readJSON(file, {});
|
|
126
|
+
addHook(config, "Stop", shellCommand(harness, "stop"), 3);
|
|
127
|
+
addHook(config, "SessionEnd", shellCommand(harness, "session_end"), 3);
|
|
128
|
+
writeJSON(file, config);
|
|
129
|
+
return file;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function enableOpenCode(harness) {
|
|
133
|
+
const file = path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"), "opencode", "plugins", "shifu-sync.js");
|
|
134
|
+
const runner = JSON.stringify(installedRunnerPath());
|
|
135
|
+
const source = `import { spawn } from "node:child_process";\n\nconst runner = ${runner};\n\nfunction notify(event) {\n const child = spawn(process.execPath, [runner, "hook", "--harness", "${harness}", "--event", "idle"], { stdio: ["pipe", "ignore", "ignore"], detached: true });\n child.stdin.end(JSON.stringify({ event }));\n child.unref();\n}\n\nexport const ShifuSync = async () => ({\n event: async ({ event }) => {\n if (event.type === "session.idle" || event.type === "session.deleted") notify(event);\n },\n});\n`;
|
|
136
|
+
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
137
|
+
fs.writeFileSync(file, source, { mode: 0o600 });
|
|
138
|
+
return file;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function installDestination(harness) {
|
|
142
|
+
if (harness === "codex") return path.join(os.homedir(), ".codex", "skills", "shifu-sync");
|
|
143
|
+
if (harness === "claude_code") return path.join(os.homedir(), ".claude", "skills", "shifu-sync");
|
|
144
|
+
return path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"), "opencode", "commands", "shifu-sync.md");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function install(harness) {
|
|
148
|
+
copyRunner();
|
|
149
|
+
const destination = installDestination(harness);
|
|
150
|
+
if (harness === "opencode") {
|
|
151
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true, mode: 0o700 });
|
|
152
|
+
fs.copyFileSync(path.join(__dirname, "..", "commands", "shifu-sync.md"), destination);
|
|
153
|
+
} else {
|
|
154
|
+
copyDirectory(path.join(__dirname, "..", "skills", "shifu-sync"), destination);
|
|
155
|
+
const skill = path.join(destination, "SKILL.md");
|
|
156
|
+
const content = fs.readFileSync(skill, "utf8")
|
|
157
|
+
.replaceAll('"codex"', `"${harness}"`)
|
|
158
|
+
.replaceAll("--harness codex", `--harness ${harness}`);
|
|
159
|
+
fs.writeFileSync(skill, content, { mode: 0o600 });
|
|
160
|
+
}
|
|
161
|
+
console.error(`Installed the Shifu sync instruction for ${HARNESS_NAMES[harness]}.`);
|
|
162
|
+
console.error(`Next: shifu-harness connect --harness ${harness}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function connect(harness) {
|
|
166
|
+
const terminal = prompt();
|
|
167
|
+
const token = (await terminal.question("Connection key: ")).trim();
|
|
168
|
+
terminal.close();
|
|
169
|
+
if (!TOKEN_PATTERN.test(token)) throw new Error("The connection key is invalid.");
|
|
170
|
+
const apiUrl = (option("--api-url") || process.env.SHIFU_API_URL || DEFAULT_API_URL).replace(/\/$/, "");
|
|
171
|
+
if (!(await confirm(`Save this ${HARNESS_NAMES[harness]} key locally for ${apiUrl}? No work content will be sent.`))) {
|
|
172
|
+
console.error("Connection was not saved.");
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const existing = readJSON(configPath(harness), {});
|
|
176
|
+
writeJSON(configPath(harness), { ...existing, apiUrl, token, harness, schedule: existing.schedule || { afterTurns: 12, afterHours: 4 } });
|
|
177
|
+
console.error(`${HARNESS_NAMES[harness]} is connected. Run activate only when you want reminders.`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function activate(harness) {
|
|
181
|
+
readConfig(harness);
|
|
182
|
+
if (!(await confirm(`Activate Shifu reminders for ${HARNESS_NAMES[harness]}? This adds local lifecycle hooks. They only mark syncs as due; they never upload automatically.`))) {
|
|
183
|
+
console.error("Activation was cancelled.");
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
copyRunner();
|
|
187
|
+
const file = harness === "codex" ? enableCodex(harness) : harness === "claude_code" ? enableClaude(harness) : enableOpenCode(harness);
|
|
188
|
+
console.error(`Shifu reminders are active. Updated ${file}.`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function schedule(harness) {
|
|
192
|
+
const config = readConfig(harness);
|
|
193
|
+
const afterTurns = Number(option("--after-turns") || config.schedule?.afterTurns || 12);
|
|
194
|
+
const afterHours = Number(option("--after-hours") || config.schedule?.afterHours || 4);
|
|
195
|
+
if (!Number.isInteger(afterTurns) || afterTurns < 1 || afterTurns > 1000 || !Number.isFinite(afterHours) || afterHours <= 0 || afterHours > 168) {
|
|
196
|
+
throw new Error("Choose --after-turns from 1 to 1000 and --after-hours greater than 0 and at most 168.");
|
|
197
|
+
}
|
|
198
|
+
if (!(await confirm(`Save a reminder after ${afterTurns} turns or ${afterHours} hours without a sync? It will request review, never upload automatically.`))) {
|
|
199
|
+
console.error("Schedule was not changed.");
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
writeJSON(configPath(harness), { ...config, schedule: { afterTurns, afterHours } });
|
|
203
|
+
console.error("Reminder schedule saved. Activate the connector to receive harness lifecycle reminders.");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function sessionID(input) {
|
|
207
|
+
return input.session_id || input.sessionId || input?.event?.properties?.sessionID || input?.event?.properties?.id;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function hook(harness, event) {
|
|
211
|
+
const config = readConfig(harness);
|
|
212
|
+
const input = readJSON(0, {});
|
|
213
|
+
const reference = sessionID(input);
|
|
214
|
+
if (typeof reference !== "string" || reference.length < 8) return;
|
|
215
|
+
const state = readState(harness);
|
|
216
|
+
state.lastSessionRef = reference;
|
|
217
|
+
const session = state.sessions[reference] || { turnsSinceSync: 0, lastSyncedAt: 0, syncDue: false };
|
|
218
|
+
if (event === "stop" || event === "idle") session.turnsSinceSync += 1;
|
|
219
|
+
if (event === "session_end") session.syncDue = true;
|
|
220
|
+
const hoursSinceSync = session.lastSyncedAt ? (Date.now() - session.lastSyncedAt) / 3_600_000 : 0;
|
|
221
|
+
const schedule = config.schedule || { afterTurns: 12, afterHours: 4 };
|
|
222
|
+
const due = session.turnsSinceSync >= schedule.afterTurns || hoursSinceSync >= schedule.afterHours || session.syncDue;
|
|
223
|
+
session.syncDue = due;
|
|
224
|
+
state.sessions[reference] = session;
|
|
225
|
+
saveState(harness, state);
|
|
226
|
+
if (event === "stop" && harness === "codex") {
|
|
227
|
+
process.stdout.write(JSON.stringify(due
|
|
228
|
+
? { systemMessage: "A Shifu sync is due. Prepare an incremental, redacted summary, show exactly what would be sent, and ask the user to confirm before running sync." }
|
|
229
|
+
: { continue: true }));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function textIsSafe(value, maximum) {
|
|
234
|
+
return typeof value === "string" && value.trim().length > 0 && value.length <= maximum && !SENSITIVE_PATTERNS.some((pattern) => value.toLowerCase().includes(pattern));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function validateSync(input, harness) {
|
|
238
|
+
if (!input || input.harness !== harness || typeof input.sessionRef !== "string" || !textIsSafe(input.sessionRef, 200) || input.sessionRef.length < 8) throw new Error("Add the current opaque sessionRef before syncing.");
|
|
239
|
+
if (!Number.isInteger(input.fromTurn) || !Number.isInteger(input.toTurn) || input.fromTurn < 1 || input.toTurn < input.fromTurn) throw new Error("Use a valid incremental turn range.");
|
|
240
|
+
if (!textIsSafe(input.summary, 1200)) throw new Error("The summary is empty, too long, or contains sensitive content. Redact it before syncing.");
|
|
241
|
+
for (const field of ["evidence", "decisions"]) {
|
|
242
|
+
if (!Array.isArray(input[field]) || input[field].length > 4 || !input[field].every((item) => textIsSafe(item, 240))) throw new Error(`${field} must contain at most four redacted statements.`);
|
|
243
|
+
}
|
|
244
|
+
if (input.redactionVersion !== 1) throw new Error("Use redactionVersion 1.");
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function sync(harness) {
|
|
248
|
+
const config = readConfig(harness);
|
|
249
|
+
const input = readJSON(0, undefined);
|
|
250
|
+
validateSync(input, harness);
|
|
251
|
+
const state = readState(harness);
|
|
252
|
+
const checkpoint = state.sessions[input.sessionRef];
|
|
253
|
+
const expected = (checkpoint?.lastSyncedTurn || 0) + 1;
|
|
254
|
+
if (input.fromTurn !== expected) throw new Error(`This local checkpoint expects turn ${expected}. Review the pending segment before retrying.`);
|
|
255
|
+
const preview = JSON.stringify(input, null, 2);
|
|
256
|
+
console.error(`\nThis exact reviewed, redacted segment will be sent to Shifu:\n${preview}`);
|
|
257
|
+
if (!(await confirm("Sync this segment"))) {
|
|
258
|
+
console.error("Nothing was sent.");
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const response = await fetch(`${config.apiUrl}/v1/connectors/coding-sessions/syncs`, {
|
|
262
|
+
method: "POST",
|
|
263
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${config.token}` },
|
|
264
|
+
body: JSON.stringify(input),
|
|
265
|
+
});
|
|
266
|
+
const payload = await response.json().catch(() => undefined);
|
|
267
|
+
if (!response.ok) throw new Error(payload?.error?.message || `Shifu rejected the sync (${response.status}).`);
|
|
268
|
+
const receipt = payload?.data;
|
|
269
|
+
if (!receipt || !Number.isInteger(receipt.lastSyncedTurn)) throw new Error("Shifu did not return a sync checkpoint.");
|
|
270
|
+
state.sessions[input.sessionRef] = { turnsSinceSync: 0, lastSyncedAt: Date.now(), lastSyncedTurn: receipt.lastSyncedTurn, syncDue: false };
|
|
271
|
+
saveState(harness, state);
|
|
272
|
+
console.error(receipt.duplicate ? "Shifu already had this exact segment. Local checkpoint recovered." : `Synced through turn ${receipt.lastSyncedTurn}.`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function status(harness) {
|
|
276
|
+
const config = readConfig(harness);
|
|
277
|
+
const state = readState(harness);
|
|
278
|
+
console.log(JSON.stringify({ harness, apiUrl: config.apiUrl, schedule: config.schedule, sessions: state.sessions }, null, 2));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function main() {
|
|
282
|
+
const command = process.argv[2];
|
|
283
|
+
const harness = requireHarness(option("--harness"));
|
|
284
|
+
if (command === "install") return install(harness);
|
|
285
|
+
if (command === "connect") return connect(harness);
|
|
286
|
+
if (command === "activate") return activate(harness);
|
|
287
|
+
if (command === "schedule") return schedule(harness);
|
|
288
|
+
if (command === "sync") return sync(harness);
|
|
289
|
+
if (command === "status") return status(harness);
|
|
290
|
+
if (command === "hook") return hook(harness, option("--event"));
|
|
291
|
+
throw new Error("Use install, connect, activate, schedule, sync, status, or hook.");
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (require.main === module) {
|
|
295
|
+
main().catch((error) => {
|
|
296
|
+
console.error(error.message);
|
|
297
|
+
process.exitCode = 1;
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
module.exports = { addHook, configPath, statePath, textIsSafe, validateSync };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Prepare a reviewed, redacted incremental Shifu sync and ask for confirmation.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Use the Shifu reviewed-sync process for the current OpenCode session. Never upload automatically or include raw transcript content. First explain what the unsynced incremental segment contains and show the exact JSON payload. Ask the user to confirm. Only after confirmation, pipe the reviewed payload to:
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npx @useshifu/coding-harness sync --harness opencode
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The payload must contain `harness`, an opaque `sessionRef`, `fromTurn`, `toTurn`, a concise redacted `summary`, up to four redacted `evidence` items, up to four redacted `decisions`, and `redactionVersion: 1`. Replace names, paths, URLs, credentials, commands, prompts, source code, customer details, and proprietary identifiers with neutral descriptions.
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@useshifu/coding-harness",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Reviewed, incremental coding-harness sync for Shifu",
|
|
5
|
+
"bin": {
|
|
6
|
+
"shifu-harness": "bin/shifu-harness.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"bin",
|
|
10
|
+
"skills",
|
|
11
|
+
"commands",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=20"
|
|
16
|
+
},
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"test": "node --test test/*.test.js"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: shifu-sync
|
|
3
|
+
description: Prepare and sync a reviewed, redacted incremental summary of the current coding session to Shifu. Use only when the user explicitly asks to connect, activate, schedule, sync, or push their Shifu coding-harness data.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Shifu captures reviewed, incremental work context. It does not collect raw transcripts or upload in the background.
|
|
7
|
+
|
|
8
|
+
Before taking any action, state the exact action and ask the user for confirmation:
|
|
9
|
+
|
|
10
|
+
- `connect`: explain that the command saves the supplied connection key locally and does not send session content.
|
|
11
|
+
- `activate`: explain that it adds local lifecycle hooks. Hooks can mark a sync as due, but never upload automatically.
|
|
12
|
+
- `schedule`: explain the turn and time thresholds. A threshold asks for review on the next active harness turn; it cannot interrupt an ended session with a dialog.
|
|
13
|
+
- `sync`: show the exact JSON payload, including its incremental `fromTurn` and `toTurn`, then ask whether to send it.
|
|
14
|
+
|
|
15
|
+
Before preparing a sync, run `npx @useshifu/coding-harness status --harness codex` to see the local checkpoint. Use its most recent opaque `lastSessionRef` and begin at `lastSyncedTurn + 1`. Do not guess a checkpoint or claim to be syncing the whole conversation when an earlier segment is still pending.
|
|
16
|
+
|
|
17
|
+
For a manual request such as “push this conversation,” prepare only the unsynced segment. Make the content concise and useful, preserving technical decisions, outcome, evidence, and verification. Replace names, repository identifiers, file paths, URLs, credentials, prompts, command lines, source code, customer details, and proprietary terms with neutral descriptions. Never invent missing details.
|
|
18
|
+
|
|
19
|
+
When a turn or time reminder appears, first decide whether the unsynced work forms a coherent, useful segment. If it does not, keep the segment pending and continue the session without prompting or sending anything. A reminder is never permission to upload.
|
|
20
|
+
|
|
21
|
+
Use this structure:
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"harness": "codex",
|
|
26
|
+
"sessionRef": "opaque-session-id",
|
|
27
|
+
"fromTurn": 1,
|
|
28
|
+
"toTurn": 12,
|
|
29
|
+
"summary": "Implemented a narrow change and checked the relevant behaviour.",
|
|
30
|
+
"evidence": ["Focused tests passed."],
|
|
31
|
+
"decisions": ["Kept the change within the existing connector boundary."],
|
|
32
|
+
"redactionVersion": 1
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Only after the user confirms the displayed payload, run:
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
printf '%s' '<reviewed JSON>' | npx @useshifu/coding-harness sync --harness codex
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The CLI prints the payload again and asks for a final terminal confirmation. Never bypass that confirmation.
|