@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 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. They are not needed for the core workflow above.
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) and unsupported for Codex 0.163.0; `host bundle` exports the bundle and `host install --mode plugin` fails closed with the per-host commands. See [host plugin validation](docs/host-plugin-validation.md).
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.7`) 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.7`, because pre-release versions are never republished.
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
- const nativeSessionFile = started.session.sessionFile();
1363
- const nativeSessionBaseline = regularFileSize(nativeSessionFile);
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
- const hostRoot = options.hostRoot === null
50
- ? undefined
51
- : resolve(options.cwd, options.hostRoot);
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',
@@ -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;
@@ -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
- await assertIdentity({ requireBinding: true });
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 = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phyzess/pappra",
3
- "version": "0.1.0-beta.7",
3
+ "version": "0.1.0-beta.9",
4
4
  "description": "Portable workspace knowledge and continuity for coding agents.",
5
5
  "publishConfig": {
6
6
  "access": "public"