@phyzess/pappra 0.1.0-beta.7 → 0.1.0-beta.9
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 +5 -3
- package/dist/app/collaboration.js +6 -13
- package/dist/cli/commands/host.d.ts +1 -0
- package/dist/cli/commands/host.js +29 -4
- package/dist/cli/run.js +18 -0
- package/dist/hosts/integration/pappra-extension-runtime.mjs +5 -0
- package/dist/hosts/integration/pappra-hook.mjs +5 -0
- package/dist/hosts/integration/support.d.ts +3 -0
- package/dist/hosts/integration/support.js +5 -0
- package/dist/hosts/pi/rpc.d.ts +2 -0
- package/dist/hosts/pi/rpc.js +39 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -55,7 +55,9 @@ configured knowledge sources.
|
|
|
55
55
|
|
|
56
56
|
Optional examples have prerequisites rather than generated files: code-graph and
|
|
57
57
|
host commands require the selected external CLI/support, and collaboration
|
|
58
|
-
requires an explicit controller.
|
|
58
|
+
requires an explicit controller. `--controller pi-rpc` is the host-directs/Pi-executes
|
|
59
|
+
path, verified end to end against Pi 1.3.0; see [Pi RPC collaboration](docs/pi-rpc.md).
|
|
60
|
+
They are not needed for the core workflow above.
|
|
59
61
|
|
|
60
62
|
## What it manages
|
|
61
63
|
|
|
@@ -63,7 +65,7 @@ requires an explicit controller. They are not needed for the core workflow above
|
|
|
63
65
|
- **Knowledge:** workspace and private document catalogs with explicit preview/apply operations.
|
|
64
66
|
- **Continuity:** private task checkpoints and bounded advisory resume packets. Resume never grants execution by itself.
|
|
65
67
|
- **Code intelligence:** status, explicit preparation and bounded query for selected repositories. Text/structure fallback is available when the optional graph engine is absent.
|
|
66
|
-
- **Host integrations:** optional Pi, Codex and Claude Code adapters that merge Pappra-owned entries without taking over user configuration. Hooks are copied as a dependency-free bundle into the host root; `host bundle --marketplace` exports a local marketplace that Claude Code and Codex accept, without registering it. Plugin hooks are verified for Pi 1.3.0 and Claude Code 2.1.292 (startup)
|
|
68
|
+
- **Host integrations:** optional Pi, Codex and Claude Code adapters that merge Pappra-owned entries without taking over user configuration. Hooks are copied as a dependency-free bundle into the host root; `host bundle --marketplace` exports a local marketplace that Claude Code and Codex accept, without registering it. Plugin hooks are verified for Pi 1.3.0 and Claude Code 2.1.292 (startup); Codex 0.163.0 loads hooks only from user scope, so use `host install --host codex --scope user` (merges `$CODEX_HOME/hooks.json`). `host bundle` exports a plugin bundle for the other hosts and `host install --mode plugin` fails closed with the per-host commands. See [host plugin validation](docs/host-plugin-validation.md).
|
|
67
69
|
- **Bounded collaboration:** an optional, explicitly controlled one-request workflow. It is not a scheduler or daemon.
|
|
68
70
|
|
|
69
71
|
## Optional integrations and support
|
|
@@ -87,7 +89,7 @@ The check runs the source policy, type checks, formatting, deterministic tests a
|
|
|
87
89
|
|
|
88
90
|
## Status and limits
|
|
89
91
|
|
|
90
|
-
Pappra is development software (`0.1.0-beta.
|
|
92
|
+
Pappra is development software (`0.1.0-beta.9`) published as `@phyzess/pappra` under the `beta` dist-tag. Releases are published from GitHub Actions with npm trusted publishing (OIDC), so no npm token is stored in this repository. Pin either the moving `beta` tag or an exact version such as `@phyzess/pappra@0.1.0-beta.9`, because pre-release versions are never republished.
|
|
91
93
|
|
|
92
94
|
The deterministic suite covers synthetic workspaces, fixtures and local process boundaries. It does not establish real-model behavior, native host callability on every platform, CodeGraph completeness, Windows/Linux parity or production performance. A host adapter version pin is an observed support claim, not proof that a particular native installation will load or run correctly.
|
|
93
95
|
|
|
@@ -461,17 +461,6 @@ function currentNativeResult(file, baseline) {
|
|
|
461
461
|
return null;
|
|
462
462
|
}
|
|
463
463
|
}
|
|
464
|
-
function regularFileSize(file) {
|
|
465
|
-
if (file === null || !path.isAbsolute(file))
|
|
466
|
-
return null;
|
|
467
|
-
try {
|
|
468
|
-
const observed = fs.lstatSync(file);
|
|
469
|
-
return observed.isFile() ? observed.size : null;
|
|
470
|
-
}
|
|
471
|
-
catch {
|
|
472
|
-
return null;
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
464
|
function writeNewFile(file, bytes) {
|
|
476
465
|
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
477
466
|
fs.writeFileSync(file, bytes, { flag: 'wx', mode: 0o600 });
|
|
@@ -1359,10 +1348,14 @@ export class PiRpcController {
|
|
|
1359
1348
|
possible_start: stage === 'identity',
|
|
1360
1349
|
};
|
|
1361
1350
|
}
|
|
1362
|
-
|
|
1363
|
-
|
|
1351
|
+
let nativeSessionFile = null;
|
|
1352
|
+
let nativeSessionBaseline = 0;
|
|
1364
1353
|
try {
|
|
1365
1354
|
await started.session.prompt();
|
|
1355
|
+
// The handle sampled the baseline before it sent the prompt; read it afterwards so
|
|
1356
|
+
// both values describe the launch that just completed.
|
|
1357
|
+
nativeSessionFile = started.session.sessionFile();
|
|
1358
|
+
nativeSessionBaseline = started.session.promptBaseline();
|
|
1366
1359
|
}
|
|
1367
1360
|
catch (error) {
|
|
1368
1361
|
await started.session.finish().catch(() => undefined);
|
|
@@ -12,5 +12,6 @@ export interface HostCommandOptions {
|
|
|
12
12
|
readonly out: string | null;
|
|
13
13
|
readonly mode: 'project-settings' | 'plugin' | null;
|
|
14
14
|
readonly marketplace: boolean;
|
|
15
|
+
readonly scope: 'project' | 'user' | null;
|
|
15
16
|
}
|
|
16
17
|
export declare function runHostCommand(options: HostCommandOptions, output: CliOutput): Promise<number>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { homedir } from 'node:os';
|
|
2
|
-
import { resolve } from 'node:path';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
3
|
import { HostIntegrationService } from '../../hosts/integration/service.js';
|
|
4
4
|
import { exportHostPluginBundle } from '../../hosts/integration/bundle-export.js';
|
|
5
5
|
import { HOST_SUPPORT, isSupportedVersion, } from '../../hosts/integration/support.js';
|
|
@@ -10,6 +10,13 @@ export const hostUsage = 'pappra host <detect|describe|plan|install|verify|bundl
|
|
|
10
10
|
'\n' +
|
|
11
11
|
'bundle exports a host-native plugin/package directory and never registers it or\n' +
|
|
12
12
|
'claims host callability.';
|
|
13
|
+
/** Codex only loads lifecycle hooks from its user configuration directory. */
|
|
14
|
+
function codexHome() {
|
|
15
|
+
const configured = process.env['CODEX_HOME'];
|
|
16
|
+
return configured === undefined || configured === ''
|
|
17
|
+
? join(homedir(), '.codex')
|
|
18
|
+
: resolve(configured);
|
|
19
|
+
}
|
|
13
20
|
function printDiagnostics(output, diagnostics) {
|
|
14
21
|
for (const entry of diagnostics) {
|
|
15
22
|
if (typeof entry === 'object' &&
|
|
@@ -46,9 +53,27 @@ export async function runHostCommand(options, output) {
|
|
|
46
53
|
hostVersion: options.hostVersion,
|
|
47
54
|
observedAt: new Date().toISOString(),
|
|
48
55
|
});
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
56
|
+
if (options.scope === 'user') {
|
|
57
|
+
if (HOST_SUPPORT[options.host].hookScope !== 'user') {
|
|
58
|
+
output.stderr(`pappra host: ${options.host} loads hooks from ${HOST_SUPPORT[options.host].hookScope} scope; --scope user is only valid for a user-scoped host (codex).\n`);
|
|
59
|
+
return 2;
|
|
60
|
+
}
|
|
61
|
+
if (options.hostRoot !== null) {
|
|
62
|
+
output.stderr('pappra host: --scope user resolves CODEX_HOME and cannot be combined with --host-root.\n');
|
|
63
|
+
return 2;
|
|
64
|
+
}
|
|
65
|
+
if (options.action !== 'plan' &&
|
|
66
|
+
options.action !== 'install' &&
|
|
67
|
+
options.action !== 'verify') {
|
|
68
|
+
output.stderr('pappra host: --scope user is only valid for plan, install, or verify.\n');
|
|
69
|
+
return 2;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const hostRoot = options.scope === 'user'
|
|
73
|
+
? codexHome()
|
|
74
|
+
: options.hostRoot === null
|
|
75
|
+
? undefined
|
|
76
|
+
: resolve(options.cwd, options.hostRoot);
|
|
52
77
|
const target = {
|
|
53
78
|
...(hostRoot === undefined ? {} : { hostRoot }),
|
|
54
79
|
...(options.hostVersion === null
|
package/dist/cli/run.js
CHANGED
|
@@ -26,6 +26,7 @@ function parseOptions(args) {
|
|
|
26
26
|
out: { type: 'string' },
|
|
27
27
|
mode: { type: 'string' },
|
|
28
28
|
marketplace: { type: 'boolean' },
|
|
29
|
+
scope: { type: 'string' },
|
|
29
30
|
bind: { type: 'boolean' },
|
|
30
31
|
force: { type: 'boolean' },
|
|
31
32
|
'no-interactive': { type: 'boolean' },
|
|
@@ -251,6 +252,16 @@ export async function runCli(args, output) {
|
|
|
251
252
|
failure(output, json, 'invalid_arguments', '--out is only valid for host.');
|
|
252
253
|
return 2;
|
|
253
254
|
}
|
|
255
|
+
if (values.scope !== undefined &&
|
|
256
|
+
values.scope !== 'project' &&
|
|
257
|
+
values.scope !== 'user') {
|
|
258
|
+
failure(output, json, 'invalid_arguments', '--scope accepts project or user.');
|
|
259
|
+
return 2;
|
|
260
|
+
}
|
|
261
|
+
if (command !== 'host' && values.scope !== undefined) {
|
|
262
|
+
failure(output, json, 'invalid_arguments', '--scope is only valid for host.');
|
|
263
|
+
return 2;
|
|
264
|
+
}
|
|
254
265
|
if (command !== 'host' && values.marketplace === true) {
|
|
255
266
|
failure(output, json, 'invalid_arguments', '--marketplace is only valid for host bundle.');
|
|
256
267
|
return 2;
|
|
@@ -665,6 +676,10 @@ export async function runCli(args, output) {
|
|
|
665
676
|
(values.apply === true && action !== 'install') ||
|
|
666
677
|
(values.mode !== undefined && action !== 'install') ||
|
|
667
678
|
(values.marketplace === true && action !== 'bundle') ||
|
|
679
|
+
(values.scope !== undefined &&
|
|
680
|
+
action !== 'plan' &&
|
|
681
|
+
action !== 'install' &&
|
|
682
|
+
action !== 'verify') ||
|
|
668
683
|
(values['host-root'] !== undefined && action === null) ||
|
|
669
684
|
(values['host-version'] !== undefined && action === null);
|
|
670
685
|
if (values.help) {
|
|
@@ -693,6 +708,9 @@ export async function runCli(args, output) {
|
|
|
693
708
|
? values.mode
|
|
694
709
|
: null,
|
|
695
710
|
marketplace: values.marketplace === true,
|
|
711
|
+
scope: values.scope === 'project' || values.scope === 'user'
|
|
712
|
+
? values.scope
|
|
713
|
+
: null,
|
|
696
714
|
}, output);
|
|
697
715
|
}
|
|
698
716
|
if (command !== null) {
|
|
@@ -23933,6 +23933,7 @@ var HOST_SUPPORT = {
|
|
|
23933
23933
|
pi: {
|
|
23934
23934
|
surface: "pi-extension",
|
|
23935
23935
|
version: "1.3.0",
|
|
23936
|
+
hookScope: "project",
|
|
23936
23937
|
events: {
|
|
23937
23938
|
startup: "session_start",
|
|
23938
23939
|
resume: "session_start",
|
|
@@ -23943,6 +23944,9 @@ var HOST_SUPPORT = {
|
|
|
23943
23944
|
codex: {
|
|
23944
23945
|
surface: "codex-hooks",
|
|
23945
23946
|
version: "0.163.0",
|
|
23947
|
+
// Codex 0.163.0 loads lifecycle hooks only from its user configuration
|
|
23948
|
+
// directory (CODEX_HOME); project `.codex/hooks.json` is not loaded.
|
|
23949
|
+
hookScope: "user",
|
|
23946
23950
|
events: {
|
|
23947
23951
|
startup: "SessionStart",
|
|
23948
23952
|
resume: "SessionStart",
|
|
@@ -23953,6 +23957,7 @@ var HOST_SUPPORT = {
|
|
|
23953
23957
|
claude: {
|
|
23954
23958
|
surface: "claude-hooks",
|
|
23955
23959
|
version: "2.1.292",
|
|
23960
|
+
hookScope: "project",
|
|
23956
23961
|
events: {
|
|
23957
23962
|
startup: "SessionStart",
|
|
23958
23963
|
resume: "SessionStart",
|
|
@@ -23938,6 +23938,7 @@ var HOST_SUPPORT = {
|
|
|
23938
23938
|
pi: {
|
|
23939
23939
|
surface: "pi-extension",
|
|
23940
23940
|
version: "1.3.0",
|
|
23941
|
+
hookScope: "project",
|
|
23941
23942
|
events: {
|
|
23942
23943
|
startup: "session_start",
|
|
23943
23944
|
resume: "session_start",
|
|
@@ -23948,6 +23949,9 @@ var HOST_SUPPORT = {
|
|
|
23948
23949
|
codex: {
|
|
23949
23950
|
surface: "codex-hooks",
|
|
23950
23951
|
version: "0.163.0",
|
|
23952
|
+
// Codex 0.163.0 loads lifecycle hooks only from its user configuration
|
|
23953
|
+
// directory (CODEX_HOME); project `.codex/hooks.json` is not loaded.
|
|
23954
|
+
hookScope: "user",
|
|
23951
23955
|
events: {
|
|
23952
23956
|
startup: "SessionStart",
|
|
23953
23957
|
resume: "SessionStart",
|
|
@@ -23958,6 +23962,7 @@ var HOST_SUPPORT = {
|
|
|
23958
23962
|
claude: {
|
|
23959
23963
|
surface: "claude-hooks",
|
|
23960
23964
|
version: "2.1.292",
|
|
23965
|
+
hookScope: "project",
|
|
23961
23966
|
events: {
|
|
23962
23967
|
startup: "SessionStart",
|
|
23963
23968
|
resume: "SessionStart",
|
|
@@ -3,6 +3,7 @@ export declare const HOST_SUPPORT: {
|
|
|
3
3
|
readonly pi: {
|
|
4
4
|
readonly surface: 'pi-extension';
|
|
5
5
|
readonly version: '1.3.0';
|
|
6
|
+
readonly hookScope: 'project';
|
|
6
7
|
readonly events: {
|
|
7
8
|
readonly startup: 'session_start';
|
|
8
9
|
readonly resume: 'session_start';
|
|
@@ -13,6 +14,7 @@ export declare const HOST_SUPPORT: {
|
|
|
13
14
|
readonly codex: {
|
|
14
15
|
readonly surface: 'codex-hooks';
|
|
15
16
|
readonly version: '0.163.0';
|
|
17
|
+
readonly hookScope: 'user';
|
|
16
18
|
readonly events: {
|
|
17
19
|
readonly startup: 'SessionStart';
|
|
18
20
|
readonly resume: 'SessionStart';
|
|
@@ -23,6 +25,7 @@ export declare const HOST_SUPPORT: {
|
|
|
23
25
|
readonly claude: {
|
|
24
26
|
readonly surface: 'claude-hooks';
|
|
25
27
|
readonly version: '2.1.292';
|
|
28
|
+
readonly hookScope: 'project';
|
|
26
29
|
readonly events: {
|
|
27
30
|
readonly startup: 'SessionStart';
|
|
28
31
|
readonly resume: 'SessionStart';
|
|
@@ -2,6 +2,7 @@ export const HOST_SUPPORT = {
|
|
|
2
2
|
pi: {
|
|
3
3
|
surface: 'pi-extension',
|
|
4
4
|
version: '1.3.0',
|
|
5
|
+
hookScope: 'project',
|
|
5
6
|
events: {
|
|
6
7
|
startup: 'session_start',
|
|
7
8
|
resume: 'session_start',
|
|
@@ -12,6 +13,9 @@ export const HOST_SUPPORT = {
|
|
|
12
13
|
codex: {
|
|
13
14
|
surface: 'codex-hooks',
|
|
14
15
|
version: '0.163.0',
|
|
16
|
+
// Codex 0.163.0 loads lifecycle hooks only from its user configuration
|
|
17
|
+
// directory (CODEX_HOME); project `.codex/hooks.json` is not loaded.
|
|
18
|
+
hookScope: 'user',
|
|
15
19
|
events: {
|
|
16
20
|
startup: 'SessionStart',
|
|
17
21
|
resume: 'SessionStart',
|
|
@@ -22,6 +26,7 @@ export const HOST_SUPPORT = {
|
|
|
22
26
|
claude: {
|
|
23
27
|
surface: 'claude-hooks',
|
|
24
28
|
version: '2.1.292',
|
|
29
|
+
hookScope: 'project',
|
|
25
30
|
events: {
|
|
26
31
|
startup: 'SessionStart',
|
|
27
32
|
resume: 'SessionStart',
|
package/dist/hosts/pi/rpc.d.ts
CHANGED
|
@@ -461,6 +461,8 @@ export declare function startOwnedPiRpc(options: PiOptions): Promise<{
|
|
|
461
461
|
pid: number | null;
|
|
462
462
|
expectedSessionId: string | null;
|
|
463
463
|
sessionFile: () => string | null;
|
|
464
|
+
/** Byte offset sampled immediately before the prompt was sent. */
|
|
465
|
+
promptBaseline: () => number;
|
|
464
466
|
/** Read-only native query. Repeatable; always recorded through the protocol. */
|
|
465
467
|
query(): Promise<{
|
|
466
468
|
at: string;
|
package/dist/hosts/pi/rpc.js
CHANGED
|
@@ -680,6 +680,7 @@ export async function startOwnedPiRpc(options) {
|
|
|
680
680
|
let reportedSessionFile = null;
|
|
681
681
|
let boundSessionFile = null;
|
|
682
682
|
let promptSent = false;
|
|
683
|
+
let promptBaseline = 0;
|
|
683
684
|
let lastPromptOrdinal = 0;
|
|
684
685
|
let operationError = null;
|
|
685
686
|
let finishPromise = null;
|
|
@@ -822,6 +823,8 @@ export async function startOwnedPiRpc(options) {
|
|
|
822
823
|
pid: child.pid ?? null,
|
|
823
824
|
expectedSessionId: admission.expected_identity,
|
|
824
825
|
sessionFile: () => boundSessionFile,
|
|
826
|
+
/** Byte offset sampled immediately before the prompt was sent. */
|
|
827
|
+
promptBaseline: () => promptBaseline,
|
|
825
828
|
/** Read-only native query. Repeatable; always recorded through the protocol. */
|
|
826
829
|
async query() {
|
|
827
830
|
try {
|
|
@@ -894,7 +897,24 @@ export async function startOwnedPiRpc(options) {
|
|
|
894
897
|
throw new PiRpcRefusal('prompt_already_sent', 'invalid', 'one frozen prompt per launch');
|
|
895
898
|
assertNamespaceGuard(opts);
|
|
896
899
|
assertCurrentAuthority({ requireResources: true });
|
|
897
|
-
|
|
900
|
+
// Pi creates the session file lazily, on the first persisted message. The
|
|
901
|
+
// pre-prompt check therefore verifies the reported session id without requiring
|
|
902
|
+
// the header; the post-prompt check requires the bound header before the run is
|
|
903
|
+
// accepted.
|
|
904
|
+
await assertIdentity({ requireBinding: false });
|
|
905
|
+
// The controller publishes only records written after this baseline, so it must
|
|
906
|
+
// be sampled before the prompt creates the first message.
|
|
907
|
+
const baselineFile = reportedSessionFile;
|
|
908
|
+
promptBaseline = (() => {
|
|
909
|
+
if (baselineFile === null)
|
|
910
|
+
return 0;
|
|
911
|
+
try {
|
|
912
|
+
return fs.statSync(baselineFile).size;
|
|
913
|
+
}
|
|
914
|
+
catch {
|
|
915
|
+
return 0;
|
|
916
|
+
}
|
|
917
|
+
})();
|
|
898
918
|
const ordinalBefore = rpc.recordCount();
|
|
899
919
|
const response = await request('prompt', { message: opts.promptText });
|
|
900
920
|
const disposition = response.data?.disposition;
|
|
@@ -902,6 +922,24 @@ export async function startOwnedPiRpc(options) {
|
|
|
902
922
|
!['started', 'queued', 'handled'].includes(disposition)) {
|
|
903
923
|
throw new PiRpcRefusal('prompt_not_accepted', 'needs_input', `disposition ${disposition}`);
|
|
904
924
|
}
|
|
925
|
+
// The header row is flushed just after the prompt is accepted, so bind with a
|
|
926
|
+
// short bounded retry instead of racing the writer.
|
|
927
|
+
const bindDeadline = Date.now() + 2_000;
|
|
928
|
+
for (;;) {
|
|
929
|
+
try {
|
|
930
|
+
await assertIdentity({ requireBinding: true });
|
|
931
|
+
break;
|
|
932
|
+
}
|
|
933
|
+
catch (error) {
|
|
934
|
+
const retryable = error instanceof PiRpcRefusal &&
|
|
935
|
+
(error.code === 'native_session_file_unavailable' ||
|
|
936
|
+
error.code === 'native_session_header_invalid') &&
|
|
937
|
+
Date.now() < bindDeadline;
|
|
938
|
+
if (!retryable)
|
|
939
|
+
throw error;
|
|
940
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
941
|
+
}
|
|
942
|
+
}
|
|
905
943
|
promptSent = true;
|
|
906
944
|
lastPromptOrdinal = ordinalBefore;
|
|
907
945
|
proof.prompt = {
|