@akagilnc/pi-workflow-roles 0.1.5216 → 0.1.5241
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/CLAUDE.md +3 -0
- package/README.md +1 -1
- package/dist/acp-host/production-host.js +658 -829
- package/dist/engine-detour-usage.js +4 -16
- package/dist/headless-host/production-host.js +777 -862
- package/dist/host-session-record.js +303 -0
- package/dist/pi/role-turn-host.js +1 -21
- package/dist/public-cli/main.js +200 -487
- package/dist/public-cli/post-admission.js +11 -60
- package/dist/sitian-appender.js +10 -155
- package/package.json +1 -1
- package/src/acp-host/role-turn-host.ts +36 -108
- package/src/engine-detour-usage.ts +4 -16
- package/src/external-host-turn-loop.ts +2 -18
- package/src/headless-host/role-turn-host.ts +67 -31
- package/src/host-contracts.ts +0 -15
- package/src/host-session-record.ts +325 -25
- package/src/notary-role.ts +0 -1
- package/src/pi/role-turn-host.ts +1 -25
- package/src/public-cli/countersign-run.ts +4 -3
- package/src/public-cli/post-admission.ts +10 -69
- package/src/public-role-summons.ts +1 -2
- package/src/role-envelope.ts +2 -2
- package/src/role-runtime.ts +0 -14
- package/src/sitian-appender.ts +16 -205
- package/dist/host-transition-prior-native.js +0 -78
- package/dist/public-cli/case-dossier-delivery.js +0 -109
- package/src/host-transition-prior-native.ts +0 -80
- package/src/pi/pi-normalization.ts +0 -291
- package/src/public-cli/case-dossier-delivery.ts +0 -147
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host conversation dossiers are native file copies; sitian append-only (ADR 0086).
|
|
3
|
+
* Replaces live event streaming (ADR 0077) with post-exit CLI original copy.
|
|
4
|
+
* Writes native-session-pointer when session ID is obtained.
|
|
5
|
+
* Writes native-session-copy (or native-session-warning on retry failure) after CLI exit.
|
|
6
|
+
* Log line write failures declare to stderr and never abort the leg.
|
|
7
|
+
*/
|
|
8
|
+
import { spawnSync } from "node:child_process";
|
|
9
|
+
import { appendFileSync, copyFileSync, closeSync, existsSync, openSync, readFileSync, mkdirSync, readdirSync, rmSync, statSync, } from "node:fs";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { dirname, join } from "node:path";
|
|
12
|
+
import { sitianReport } from "./sitian-facade.js";
|
|
13
|
+
/** Volume category under `<run>/session/<kind>/records.jsonl`. */
|
|
14
|
+
export const HOST_SESSION_RECORD_KIND = "host-session";
|
|
15
|
+
function declareHostSessionFailure(error) {
|
|
16
|
+
const message = error instanceof Error ? (error.stack || error.message) : String(error);
|
|
17
|
+
process.stderr.write(`[host-session] Dossier record failure: ${message}\n`);
|
|
18
|
+
}
|
|
19
|
+
/** Declare sitian record failure once to stderr without aborting the leg (ADR 0086). */
|
|
20
|
+
export function sitianReportSafe(input) {
|
|
21
|
+
try {
|
|
22
|
+
sitianReport(input);
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
declareHostSessionFailure(error);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** Recursively scan for Codex rollout file matching sessionId under sessionsDir. */
|
|
29
|
+
function findCodexRollout(sessionsDir, sessionId) {
|
|
30
|
+
if (!existsSync(sessionsDir))
|
|
31
|
+
return undefined;
|
|
32
|
+
const matches = [];
|
|
33
|
+
function scan(dir, depth = 0) {
|
|
34
|
+
if (depth > 6)
|
|
35
|
+
return;
|
|
36
|
+
let entries;
|
|
37
|
+
try {
|
|
38
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
for (const entry of entries) {
|
|
44
|
+
const fullPath = join(dir, entry.name);
|
|
45
|
+
if (entry.isDirectory()) {
|
|
46
|
+
scan(fullPath, depth + 1);
|
|
47
|
+
}
|
|
48
|
+
else if (entry.isFile() && entry.name.endsWith(".jsonl") && entry.name.includes(sessionId)) {
|
|
49
|
+
try {
|
|
50
|
+
const stat = statSync(fullPath);
|
|
51
|
+
matches.push({ path: fullPath, mtime: stat.mtimeMs });
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
matches.push({ path: fullPath, mtime: 0 });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
scan(sessionsDir);
|
|
60
|
+
if (matches.length === 0 || matches[0] === undefined)
|
|
61
|
+
return undefined;
|
|
62
|
+
matches.sort((a, b) => b.mtime - a.mtime);
|
|
63
|
+
return matches[0]?.path;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Sanitize working directory to match Claude Code's project directory name layout.
|
|
67
|
+
* Claude Code replaces all non-alphanumeric characters (slashes, dots, underscores, spaces, etc.) with '-'.
|
|
68
|
+
*/
|
|
69
|
+
export function sanitizeClaudeCwd(cwd) {
|
|
70
|
+
return cwd.replace(/[^a-zA-Z0-9]/g, "-");
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Resolve the CLI native session original path.
|
|
74
|
+
* Codex: single file rollout from ~/.codex/sessions/** /rollout-*-${sessionId}.jsonl
|
|
75
|
+
* Claude: single file transcript from ~/.claude/projects/<sanitized-cwd>/${sessionId}.jsonl
|
|
76
|
+
* Grok: directory ~/.grok/sessions/<encoded-cwd>/${sessionId}
|
|
77
|
+
* Pi / Hermes: undefined (Pi directly lands in session.jsonl; Hermes is state.db excluded from ADR 0086)
|
|
78
|
+
*/
|
|
79
|
+
export function resolveNativeSessionPath(options) {
|
|
80
|
+
const home = options.home || homedir();
|
|
81
|
+
if (options.host === "claude") {
|
|
82
|
+
const configDir = process.env.CLAUDE_CONFIG_DIR || join(home, ".claude");
|
|
83
|
+
const sanitizedCwd = sanitizeClaudeCwd(options.cwd);
|
|
84
|
+
return join(configDir, "projects", sanitizedCwd, `${options.sessionId}.jsonl`);
|
|
85
|
+
}
|
|
86
|
+
if (options.host === "codex") {
|
|
87
|
+
return findCodexRollout(codexSessionsDirectory(home), options.sessionId);
|
|
88
|
+
}
|
|
89
|
+
if (options.host === "grok-build") {
|
|
90
|
+
const grokHome = process.env.GROK_HOME || join(home, ".grok");
|
|
91
|
+
return join(grokHome, "sessions", encodeURIComponent(options.cwd), options.sessionId);
|
|
92
|
+
}
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
function codexSessionsDirectory(home) {
|
|
96
|
+
return join(process.env.CODEX_HOME || join(home || homedir(), ".codex"), "sessions");
|
|
97
|
+
}
|
|
98
|
+
/** Count each host activation, including Pi turns without an external copy. */
|
|
99
|
+
export function recordRunStart(runDirectory) {
|
|
100
|
+
try {
|
|
101
|
+
appendFileSync(join(runDirectory, ".run-starts"), "\n");
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
declareHostSessionFailure(error);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Resolve the host dossier landing destination under `<run>/session/`.
|
|
109
|
+
* Format: `<run>/session/<host>-<model>-<n>` (single file adds .jsonl; model slashes replaced with -).
|
|
110
|
+
* Ordinal n is the start/resume count in the run (initial 1, each resume +1).
|
|
111
|
+
*/
|
|
112
|
+
export function resolveHostDossierLandingPath(options) {
|
|
113
|
+
const sanitizedModel = (options.model?.model ?? "default").replace(/[\/\\]+/g, "-");
|
|
114
|
+
let maxOrdinal = 0;
|
|
115
|
+
if (existsSync(options.sessionDirectory)) {
|
|
116
|
+
try {
|
|
117
|
+
const entries = readdirSync(options.sessionDirectory);
|
|
118
|
+
for (const entry of entries) {
|
|
119
|
+
const match = entry.match(/-(\d+)(?:\.jsonl|\.failed)?$/);
|
|
120
|
+
const first = match?.[1];
|
|
121
|
+
if (first !== undefined) {
|
|
122
|
+
const val = parseInt(first, 10);
|
|
123
|
+
if (!isNaN(val) && val > maxOrdinal) {
|
|
124
|
+
maxOrdinal = val;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// Ignore directory read errors
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const startsPath = join(dirname(options.sessionDirectory), ".run-starts");
|
|
134
|
+
let starts = 0;
|
|
135
|
+
if (existsSync(startsPath))
|
|
136
|
+
starts = readFileSync(startsPath, "utf8").length;
|
|
137
|
+
const ordinal = Math.max(maxOrdinal + 1, starts, options.continuation.kind === "resume" ? 2 : 1);
|
|
138
|
+
const baseName = `${options.host}-${sanitizedModel}-${ordinal}`;
|
|
139
|
+
const landingPath = options.host === "grok-build"
|
|
140
|
+
? join(options.sessionDirectory, baseName)
|
|
141
|
+
: join(options.sessionDirectory, `${baseName}.jsonl`);
|
|
142
|
+
return { landingPath, ordinal, sanitizedModel };
|
|
143
|
+
}
|
|
144
|
+
/** Record native session pointer line to Sitian as soon as session ID is known. */
|
|
145
|
+
export function recordNativeSessionPointer(options) {
|
|
146
|
+
if (options.host === "pi" || options.host === "hermes") {
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
const nativePath = options.host === "codex"
|
|
150
|
+
? codexSessionsDirectory(options.home)
|
|
151
|
+
: resolveNativeSessionPath(options);
|
|
152
|
+
if (nativePath === undefined)
|
|
153
|
+
return undefined;
|
|
154
|
+
sitianReportSafe({
|
|
155
|
+
level: "event",
|
|
156
|
+
kind: HOST_SESSION_RECORD_KIND,
|
|
157
|
+
host: options.host,
|
|
158
|
+
cwd: options.cwd,
|
|
159
|
+
sessionParent: options.sessionParent,
|
|
160
|
+
source: `${options.host}-dossier`,
|
|
161
|
+
payload: {
|
|
162
|
+
type: "native-session-pointer",
|
|
163
|
+
nativePath,
|
|
164
|
+
sessionId: options.sessionId,
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
return nativePath;
|
|
168
|
+
}
|
|
169
|
+
/** File clone via APFS `cp -c` with fallback to `copyFileSync`. */
|
|
170
|
+
function copyFileCloneOrFallback(src, dest) {
|
|
171
|
+
try {
|
|
172
|
+
const res = spawnSync("cp", ["-c", src, dest]);
|
|
173
|
+
if (res.status === 0)
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
// cp execution failed, fall back
|
|
178
|
+
}
|
|
179
|
+
copyFileSync(src, dest);
|
|
180
|
+
}
|
|
181
|
+
/** Copy both grok-build originals: chat_history.jsonl and usage.json into destDir. */
|
|
182
|
+
function copyGrokDossier(srcDir, destDir) {
|
|
183
|
+
const chatHistorySrc = join(srcDir, "chat_history.jsonl");
|
|
184
|
+
if (!existsSync(chatHistorySrc)) {
|
|
185
|
+
throw new Error(`Grok native chat_history.jsonl missing at ${chatHistorySrc}`);
|
|
186
|
+
}
|
|
187
|
+
mkdirSync(destDir, { recursive: true });
|
|
188
|
+
const chatHistoryDest = join(destDir, "chat_history.jsonl");
|
|
189
|
+
copyFileCloneOrFallback(chatHistorySrc, chatHistoryDest);
|
|
190
|
+
const usageSrc = join(srcDir, "usage.json");
|
|
191
|
+
if (!existsSync(usageSrc)) {
|
|
192
|
+
throw new Error(`Grok native usage.json missing at ${usageSrc}`);
|
|
193
|
+
}
|
|
194
|
+
copyFileCloneOrFallback(usageSrc, join(destDir, "usage.json"));
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Copy native host session original to run session dossier after child process exits.
|
|
198
|
+
* If copy fails, retries once.
|
|
199
|
+
* On success, appends native-session-copy line to Sitian.
|
|
200
|
+
* On repeated failure, appends native-session-warning line to Sitian.
|
|
201
|
+
* Does not throw; turn outcome is never altered by copy success or failure.
|
|
202
|
+
*/
|
|
203
|
+
export function copyAndRecordHostDossier(options) {
|
|
204
|
+
if (options.host === "pi" || options.host === "hermes") {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const nativePath = resolveNativeSessionPath({
|
|
208
|
+
host: options.host,
|
|
209
|
+
sessionId: options.sessionId,
|
|
210
|
+
cwd: options.cwd,
|
|
211
|
+
...(options.home !== undefined ? { home: options.home } : {}),
|
|
212
|
+
});
|
|
213
|
+
if (nativePath === undefined && options.host !== "codex")
|
|
214
|
+
return;
|
|
215
|
+
let destination;
|
|
216
|
+
try {
|
|
217
|
+
destination = resolveHostDossierLandingPath(options);
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
sitianReportSafe({
|
|
221
|
+
level: "event",
|
|
222
|
+
kind: HOST_SESSION_RECORD_KIND,
|
|
223
|
+
host: options.host,
|
|
224
|
+
cwd: options.cwd,
|
|
225
|
+
sessionParent: options.sessionParent,
|
|
226
|
+
source: `${options.host}-dossier`,
|
|
227
|
+
payload: {
|
|
228
|
+
type: "native-session-warning",
|
|
229
|
+
sessionId: options.sessionId,
|
|
230
|
+
error: error instanceof Error ? error.message : String(error),
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const { landingPath, ordinal, sanitizedModel } = destination;
|
|
236
|
+
let lastError;
|
|
237
|
+
let copySuccess = false;
|
|
238
|
+
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
239
|
+
try {
|
|
240
|
+
if (options.host === "grok-build") {
|
|
241
|
+
copyGrokDossier(nativePath, landingPath);
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
if (nativePath === undefined || !existsSync(nativePath)) {
|
|
245
|
+
throw new Error(`Native session file missing for ${options.host} session ${options.sessionId}`);
|
|
246
|
+
}
|
|
247
|
+
mkdirSync(dirname(landingPath), { recursive: true });
|
|
248
|
+
copyFileCloneOrFallback(nativePath, landingPath);
|
|
249
|
+
}
|
|
250
|
+
copySuccess = true;
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
lastError = error;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (copySuccess) {
|
|
258
|
+
sitianReportSafe({
|
|
259
|
+
level: "event",
|
|
260
|
+
kind: HOST_SESSION_RECORD_KIND,
|
|
261
|
+
host: options.host,
|
|
262
|
+
cwd: options.cwd,
|
|
263
|
+
sessionParent: options.sessionParent,
|
|
264
|
+
source: `${options.host}-dossier`,
|
|
265
|
+
payload: {
|
|
266
|
+
type: "native-session-copy",
|
|
267
|
+
nativePath,
|
|
268
|
+
landingPath,
|
|
269
|
+
ordinal,
|
|
270
|
+
sessionId: options.sessionId,
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
// A partial first attempt must not masquerade as a complete native original.
|
|
276
|
+
try {
|
|
277
|
+
rmSync(landingPath, { recursive: options.host === "grok-build", force: true });
|
|
278
|
+
}
|
|
279
|
+
catch { /* warning below reports the original copy failure */ }
|
|
280
|
+
// Reserve the failed ordinal outside the native dossier landing namespace.
|
|
281
|
+
try {
|
|
282
|
+
mkdirSync(options.sessionDirectory, { recursive: true });
|
|
283
|
+
closeSync(openSync(join(options.sessionDirectory, `${options.host}-${sanitizedModel}-${ordinal}.failed`), "a"));
|
|
284
|
+
}
|
|
285
|
+
catch { /* the warning below retains the original copy failure */ }
|
|
286
|
+
sitianReportSafe({
|
|
287
|
+
level: "event",
|
|
288
|
+
kind: HOST_SESSION_RECORD_KIND,
|
|
289
|
+
host: options.host,
|
|
290
|
+
cwd: options.cwd,
|
|
291
|
+
sessionParent: options.sessionParent,
|
|
292
|
+
source: `${options.host}-dossier`,
|
|
293
|
+
payload: {
|
|
294
|
+
type: "native-session-warning",
|
|
295
|
+
nativePath,
|
|
296
|
+
landingPath,
|
|
297
|
+
ordinal,
|
|
298
|
+
sessionId: options.sessionId,
|
|
299
|
+
error: lastError instanceof Error ? lastError.message : String(lastError),
|
|
300
|
+
},
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
}
|
|
@@ -11,7 +11,6 @@ import { platform } from "node:process";
|
|
|
11
11
|
import { promisify } from "node:util";
|
|
12
12
|
import { randomUUID } from "node:crypto";
|
|
13
13
|
import { ExplicitInternalActivationError } from "../host-contracts.js";
|
|
14
|
-
import { isOfficerReviewSeat } from "../packaged-role-registry.js";
|
|
15
14
|
import { applyEngineChildEnv, ENGINE_MODEL_FLAG_NAME, normalizeEngineName } from "../engine-detour.js";
|
|
16
15
|
import { projectActivationFlags } from "../role-activation-flags.js";
|
|
17
16
|
import { encodeUserDialogueStdin } from "../user-dialogue-stdin.js";
|
|
@@ -278,26 +277,7 @@ export function createPiRoleTurnHost(config) {
|
|
|
278
277
|
});
|
|
279
278
|
return {
|
|
280
279
|
async executeTurn(request) {
|
|
281
|
-
|
|
282
|
-
// Pi already owns its own session file, so only sitian prior volume rides in.
|
|
283
|
-
// #879: station-child officer dialogue keeps peer words — do not splice
|
|
284
|
-
// host-transition priorNativePaths into the review prompt body.
|
|
285
|
-
let turnRequest = request;
|
|
286
|
-
const officerStationChild = request.stationChild === true && isOfficerReviewSeat(request.activation.role);
|
|
287
|
-
const paths = !officerStationChild && request.hostTransition?.priorNativeKind === "sitian"
|
|
288
|
-
? request.hostTransition.priorNativePaths
|
|
289
|
-
: undefined;
|
|
290
|
-
if (request.continuation.kind === "resume"
|
|
291
|
-
&& paths !== undefined
|
|
292
|
-
&& paths.length > 0) {
|
|
293
|
-
turnRequest = {
|
|
294
|
-
...request,
|
|
295
|
-
continuation: {
|
|
296
|
-
...request.continuation,
|
|
297
|
-
prompt: `${request.continuation.prompt}\n${paths.join("\n")}`,
|
|
298
|
-
},
|
|
299
|
-
};
|
|
300
|
-
}
|
|
280
|
+
const turnRequest = request;
|
|
301
281
|
const roleEntry = await realpath(resolveInternalRoleEntrypoint(config.packageRoot));
|
|
302
282
|
const extraArgs = buildPiTurnExtraArgs(turnRequest, config.principalAuthority, config.extraPiArgs ?? []);
|
|
303
283
|
const args = buildExplicitInternalActivationArgs(roleEntry, extraArgs);
|