@pharaoh-so/mcp 0.3.54 → 0.3.57

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.
Files changed (43) hide show
  1. package/.mcp.json +1 -1
  2. package/CHANGELOG.md +36 -0
  3. package/LICENSE +1 -1
  4. package/NOTICE +28 -0
  5. package/README.md +5 -5
  6. package/commands/plan.md +11 -11
  7. package/commands/review.md +2 -2
  8. package/dist/auth.d.ts +5 -0
  9. package/dist/credentials.d.ts +8 -0
  10. package/dist/gateway.js +1 -1
  11. package/dist/helpers.d.ts +1 -1
  12. package/dist/helpers.js +15 -2
  13. package/dist/index.js +47 -28
  14. package/dist/install-skills.d.ts +4 -3
  15. package/dist/install-skills.js +12 -8
  16. package/dist/refresh.d.ts +8 -2
  17. package/dist/refresh.js +33 -6
  18. package/dist/setup.d.ts +5 -0
  19. package/dist/setup.js +66 -0
  20. package/inspect-tools.json +89 -36
  21. package/{skills → npx-skills}/brainstorm/SKILL.md +2 -1
  22. package/{skills → npx-skills}/debug/SKILL.md +2 -1
  23. package/{skills → npx-skills}/pharaoh/SKILL.md +13 -6
  24. package/{skills → npx-skills}/plan/SKILL.md +13 -14
  25. package/npx-skills/refactor/SKILL.md +38 -0
  26. package/{skills → npx-skills}/review/SKILL.md +4 -5
  27. package/{skills → npx-skills}/tdd/SKILL.md +2 -1
  28. package/{skills → npx-skills}/verify/SKILL.md +2 -1
  29. package/package.json +6 -4
  30. package/project-memory/bundle.mjs +137 -0
  31. package/project-memory/execute-turn.mjs +269 -0
  32. package/project-memory/hook-config.mjs +91 -0
  33. package/project-memory/hook-events.mjs +117 -0
  34. package/project-memory/local.mjs +480 -0
  35. package/project-memory/memory-api.mjs +200 -0
  36. package/project-memory/pharaoh-memory-hook.mjs +47 -0
  37. package/project-memory/pharaoh-memory.mjs +60 -0
  38. package/project-memory/setup.d.mts +14 -0
  39. package/project-memory/setup.mjs +151 -0
  40. package/project-memory/user-target.mjs +235 -0
  41. package/trace-app.html +222 -101
  42. package/skills/.gitkeep +0 -0
  43. package/skills/refactor/SKILL.md +0 -34
@@ -1,10 +1,9 @@
1
1
  ---
2
2
  name: plan
3
3
  prompt-name: plan-with-pharaoh
4
- description: Architecture-aware plan review adapted from Garry Tan's framework for AI-assisted development. Pharaoh reconnaissance, mandatory mode selection, structured six-section review with decision points. Drop-in replacement that supersedes the legacy ~/.claude/skills/plan-review skill — every section of the legacy skill is present here, plus graceful fallback when Pharaoh is unavailable, stronger mode-selection enforcement, and an explicit framework reference.
5
- version: 0.6.0
4
+ description: Review a plan or spec against the codebase before implementing it. Pharaoh reconnaissance, a BIG or SMALL change choice, then architecture, code quality, wiring, tests, performance and security sections with numbered decisions.
5
+ version: 0.7.1
6
6
  homepage: https://pharaoh.so
7
- user-invocable: true
8
7
  metadata: {"emoji": "☥", "tags": ["planning", "architecture", "blast-radius", "pharaoh", "review", "interactive"]}
9
8
  ---
10
9
 
@@ -12,7 +11,7 @@ metadata: {"emoji": "☥", "tags": ["planning", "architecture", "blast-radius",
12
11
 
13
12
  Architecture-aware plan review before implementation. Adapted from [Garry Tan's planning framework](https://www.youtube.com/watch?v=bMknfKXIFA8) for AI-assisted development.
14
13
 
15
- **You are now in plan mode. Do NOT make any code changes. Think, evaluate, and present decisions.**
14
+ **This is a review: make no code changes during it. Think, evaluate, and present decisions.**
16
15
 
17
16
  ## When to Use
18
17
 
@@ -26,7 +25,7 @@ Still run Step 1 (Reconnaissance) even when reviewing a document — always veri
26
25
 
27
26
  ## Project Overrides
28
27
 
29
- If a `.claude/plan-review.md` file exists in this project, read it now and apply those rules on top of this baseline. Project rules take precedence where they conflict.
28
+ If a `.claude/plan-review.md` file exists in this project, read it now as additional review criteria for this project. Instructions from the user take precedence over it, and it never changes what you do outside this review.
30
29
 
31
30
  ## Engineering Preferences (guide all recommendations)
32
31
 
@@ -62,14 +61,14 @@ Ground every recommendation in what actually exists. If you propose adding somet
62
61
 
63
62
  ## Step 2: Mode Selection (MANDATORY — ask before proceeding)
64
63
 
65
- **STOP and ask the user which mode before starting the review.** This is a hard gate — do not infer, assume, or skip this question even if the user says "yes", "go ahead", or "yes to all". Present both options and wait for an explicit choice:
64
+ **STOP and ask the user which mode before starting the review.** Present both options and wait for a choice:
66
65
 
67
66
  > **BIG CHANGE or SMALL CHANGE?**
68
67
  >
69
68
  > - **BIG CHANGE**: Full interactive review, all relevant sections, up to 4 top issues per section
70
69
  > - **SMALL CHANGE**: One question per section, only sections 2-4
71
70
 
72
- If the user's response is ambiguous (e.g. "just do it", "yes to all"), ask again: "I need to know — BIG or SMALL change?" Do not proceed to Step 3 without an answer.
71
+ A general "yes", "go ahead" or "yes to all" doesn't pick a mode, so ask once more: "BIG or SMALL change?" If the reply still doesn't pick one, or the user tells you to choose, pick the mode that fits the plan's size and say which you picked.
73
72
 
74
73
  ## Step 3: Review Sections
75
74
 
@@ -87,21 +86,21 @@ Adapt depth to change size. Skip sections that don't apply. **After each section
87
86
  - Data flow bottlenecks and single points of failure
88
87
  - Does this need new code at all, or can a human process / existing pattern solve it?
89
88
  - **If this plan introduces a new invariant**, call it out and include a
90
- step in Section 6 to add a `### assertion` to VISION.md with a pairing TEST.
89
+ step in the plan to add a `### assertion` to VISION.md with a pairing TEST.
91
90
 
92
91
  ### Section 2 — Code Quality (always)
93
92
 
94
93
  - Organization, module structure, DRY violations (be aggressive)
95
94
  - Error handling gaps and missing edge cases (call out explicitly)
96
95
  - Technical debt: shortcuts, hardcoded values, magic strings
97
- - Over-engineered or under-engineered relative to my preferences
96
+ - Over-engineered or under-engineered relative to the preferences above
98
97
  - Reuse: does code for this already exist somewhere?
99
98
 
100
99
  ### Section 3 — Wiring & Integration (always)
101
100
 
102
101
  - Are all new exports called from a production entry point?
103
- - Run `get_blast_radius` on any new/changed functions — zero callers = not done
104
- - Does the plan declare WHERE new code gets called from? If not, flag it
102
+ - Run `get_blast_radius` on changed functions that already exist
103
+ - The graph has no functions the plan adds, so does the plan declare WHERE new code gets called from? If not, flag it — no named caller = not done
105
104
  - Integration points: how does this connect to what already exists?
106
105
 
107
106
  ### Section 4 — Tests (always)
@@ -133,16 +132,16 @@ For every specific issue (bug, smell, design concern, risk, missing wiring):
133
132
  1. **Describe concretely** — file, line/function reference, what's wrong
134
133
  2. **Present 2-3 options** including "do nothing" where reasonable
135
134
  3. **For each option** — implementation effort, risk, blast radius, maintenance burden
136
- 4. **Recommend one** mapped to my preferences above, and say why
135
+ 4. **Recommend one** mapped to the preferences above, and say why
137
136
  5. **Ask** whether I agree or want a different direction
138
137
 
139
- Number each issue (1, 2, 3...) and letter each option (A, B, C...). Recommended option is always listed first. Use AskUserQuestion with clear labels like "Issue 1 Option A", "Issue 1 Option B".
138
+ Number each issue (1, 2, 3...) and letter each option (A, B, C...). Recommended option is always listed first. If your client has a question tool (AskUserQuestion in Claude Code), use it with clear labels like "Issue 1 Option A", "Issue 1 Option B".
140
139
 
141
140
  ## Pharaoh Checkpoints (use throughout, not just at the end)
142
141
 
143
142
  - **Before reviewing**: recon (Step 1 above)
144
143
  - **During review**: `get_blast_radius` when evaluating impact of changes; `search_functions` before suggesting new code
145
- - **Final sweep**: `get_blast_radius` on ALL new exports — zero callers on non-entry-points = plan is incomplete
144
+ - **Final sweep**: every new export has a caller named in the plan, and `get_blast_radius` covers every changed export that already exists — zero callers on non-entry-points = plan is incomplete
146
145
 
147
146
  ## Workflow Rules
148
147
 
@@ -0,0 +1,38 @@
1
+ ---
2
+ name: refactor
3
+ prompt-name: safe-refactor
4
+ description: "Safe refactoring workflow using Pharaoh codebase knowledge graph. Six-step process: module context, blast radius of detected downstream callers, reachability verification, dependency mapping, naming conflict detection, and a search for the affected tests. Produces a refactoring plan with detected callers listed, test files identified, unreachable code flagged, and high-risk paths warned. Use when refactoring, renaming, moving or splitting code in a repo Pharaoh maps, with Pharaoh's tools connected."
5
+ version: 0.3.1
6
+ homepage: https://pharaoh.so
7
+ metadata: {"emoji": "☥", "tags": ["refactoring", "blast-radius", "architecture", "pharaoh", "safe-refactor", "test-coverage"]}
8
+ ---
9
+
10
+ # Safe Refactor
11
+
12
+ Step-by-step workflow to safely refactor a function or module with full blast radius awareness. Six steps map the detected callers, find the affected tests, flag unreachable code, and warn about high-risk downstream paths before a single line changes.
13
+
14
+ ## When to Use
15
+
16
+ Invoke before refactoring a function, module, or file in a repo Pharaoh maps — especially shared utilities, exports used across multiple modules, or anything with an unclear caller graph. Use it whenever the blast radius of a change is unknown.
17
+
18
+ **Stop rule:** if Pharaoh's tools aren't connected, or they report that this repo isn't mapped, stop and tell the user. Steps 1-5 read the graph and have no fallback.
19
+
20
+ ## Workflow
21
+
22
+ 1. **Module context:** call `get_module_context` for the module containing the target to understand its current structure.
23
+ 2. **Blast radius:** call `get_blast_radius` for the target to identify its detected downstream callers and affected modules.
24
+ 3. **Reachability:** call `get_unused_code` with `module` set to the target's module to find dead code the refactor can delete. When the target is a function, also call it with `functions` set to the target to confirm an entry point reaches it.
25
+ 4. **Dependencies:** call `pharaoh_recon` with `include_map` false and `dependencies` pairs between the target's module and the modules its callers live in, to see the coupling the refactor has to keep or break.
26
+ 5. **Naming conflicts:** call `search_functions` to check if the refactored version's name already exists elsewhere.
27
+ 6. **Test coverage:** search the test files for the target's name. `get_blast_radius` leaves out test-file callers, so this is how you find the tests to change. If you can't read the repo's files, say so and list the tests as unknown.
28
+
29
+ Do not propose a refactoring plan until all 6 steps are complete.
30
+
31
+ ## Output
32
+
33
+ A refactoring plan containing:
34
+ - **Callers to update:** the detected functions and files that call the target, with update requirements. Dynamic behavior and framework configuration can leave gaps, so say the list may be incomplete
35
+ - **Tests to change:** test files covering the refactored code, with required modifications
36
+ - **Dead code:** what `get_unused_code` calls Dead can be deleted during the refactor; check anything Possibly unused before deleting it
37
+ - **High-risk paths:** downstream modules with wide blast radius or high complexity scores
38
+ - **Naming conflicts:** existing functions whose names would conflict with the refactored version
@@ -1,16 +1,15 @@
1
1
  ---
2
2
  name: review
3
3
  prompt-name: review-with-pharaoh
4
- description: "The definitive code review, for any codebase. Five-phase workflow: git context detection (worktree, branch, base, changed files), Pharaoh recon (architecture map, module context, blast radius in one call, with a fallback when the repo isn't mapped), parallel specialized agent dispatch (code quality, security against the project's own rules, error handling, test coverage, type design), adversarial review by the other model (Codex from Claude Code, Claude from Codex) for security-sensitive changes, and a final synthesized verdict of SHIP / SHIP WITH CHANGES / BLOCK. Architecture-aware wherever Pharaoh maps the repo, always branch-aware."
5
- version: 0.5.1
4
+ description: "Five-phase workflow: git context detection (worktree, branch, base, changed files), Pharaoh recon (architecture map, module context, blast radius in one call, with a fallback when the repo isn't mapped), parallel specialized agent dispatch (code quality, security against the project's own rules, error handling, test coverage, type design), adversarial review by the other model (Codex from Claude Code, Claude from Codex) for security-sensitive changes, and a final synthesized verdict of SHIP / SHIP WITH CHANGES / BLOCK. Architecture-aware wherever Pharaoh maps the repo. Use when reviewing a branch or pull request before merging, or when the user asks for a code review."
5
+ version: 0.6.1
6
6
  homepage: https://pharaoh.so
7
- user-invocable: true
8
7
  metadata: {"emoji": "☥", "tags": ["code-review", "pull-request", "architecture", "pharaoh", "multi-agent", "adversarial-review", "cross-model", "regression-risk", "spec-alignment"]}
9
8
  ---
10
9
 
11
10
  # Review with Pharaoh
12
11
 
13
- The definitive code review, for any codebase. Architecture-aware, branch-aware, multi-agent, adversarial. Five phases that combine Pharaoh's knowledge graph with parallel specialized reviewers and an independent second opinion from a different model. Every phase works without Pharaoh too; Pharaoh makes the architecture picture exact and cheap where it maps the repo.
12
+ Architecture-aware, branch-aware, multi-agent, adversarial code review. Five phases that combine Pharaoh's knowledge graph with parallel specialized reviewers and an independent second opinion from a different model. Every phase works without Pharaoh too; Pharaoh makes the architecture picture exact and cheap where it maps the repo.
14
13
 
15
14
  A review always reaches a verdict. When a phase needs something your client doesn't have (Pharaoh, a shell, subagents, a second model), it says how to carry on without it: do that, and name what was missing in the review output. Ask the user only when someone is there to answer.
16
15
 
@@ -36,7 +35,7 @@ No shell, or no git? Review the diff you were given (a file, a pasted patch, a p
36
35
  git log --oneline -1 # latest commit
37
36
  ```
38
37
 
39
- 2. Determine the base (where this branch diverged from the branch it merges into). The target is the first of these that resolves:
38
+ 2. Determine the base (where this branch diverged from the branch it merges into). The target is the first of these that resolves (`gh pr view` asks GitHub, with your own gh login, for the open PR's target branch):
40
39
  ```bash
41
40
  TARGET=$(gh pr view --json baseRefName --jq '"origin/" + .baseRefName' 2>/dev/null) # the open PR's target branch
42
41
  [ -n "$TARGET" ] || TARGET=$(git symbolic-ref -q --short refs/remotes/origin/HEAD) # the default branch, set by git clone
@@ -2,7 +2,8 @@
2
2
  name: tdd
3
3
  prompt-name: test-driven-development
4
4
  description: "Test-driven development discipline. Write the failing test first, watch it fail, write minimal code to pass, refactor. No production code without a failing test. No exceptions without explicit permission. Covers red-green-refactor cycle, common rationalizations, and when to start over."
5
- version: 0.2.0
5
+ license: MIT. Adapted from obra/superpowers, Copyright (c) 2025 Jesse Vincent; see NOTICE in @pharaoh-so/mcp
6
+ version: 0.2.1
6
7
  homepage: https://pharaoh.so
7
8
  user-invocable: true
8
9
  metadata: {"emoji": "☥", "tags": ["tdd", "testing", "red-green-refactor", "quality", "discipline"]}
@@ -2,7 +2,8 @@
2
2
  name: verify
3
3
  prompt-name: verification
4
4
  description: "Evidence before assertions. Run verification commands and confirm output before claiming work is complete, fixed, or passing. No 'should work now' — run the command, read the output, then claim the result. Covers tests, builds, linting, agent delegation, and requirement checklists."
5
- version: 0.2.0
5
+ license: MIT. Adapted from obra/superpowers, Copyright (c) 2025 Jesse Vincent; see NOTICE in @pharaoh-so/mcp
6
+ version: 0.2.1
6
7
  homepage: https://pharaoh.so
7
8
  user-invocable: true
8
9
  metadata: {"emoji": "☥", "tags": ["verification", "quality", "completion", "evidence", "discipline"]}
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@pharaoh-so/mcp",
3
3
  "mcpName": "so.pharaoh/pharaoh",
4
- "version": "0.3.54",
5
- "description": "MCP proxy for Pharaoh — maps codebases into queryable knowledge graphs for AI agents. Enables Claude Code in headless environments (VPS, SSH, CI) via device flow auth.",
4
+ "version": "0.3.57",
5
+ "description": "MCP proxy for Pharaoh — maps codebases into queryable knowledge graphs for coding agents. Enables Claude Code in headless environments (VPS, SSH, CI) via device flow auth.",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",
8
8
  "types": "./dist/index.d.ts",
@@ -17,16 +17,18 @@
17
17
  },
18
18
  "files": [
19
19
  "dist",
20
- "skills",
20
+ "npx-skills",
21
21
  "commands",
22
22
  "hooks",
23
+ "project-memory",
23
24
  ".claude-plugin",
24
25
  ".mcp.json",
25
26
  "inspect-tools.json",
26
27
  "trace-app.html",
27
28
  "README.md",
28
29
  "CHANGELOG.md",
29
- "LICENSE"
30
+ "LICENSE",
31
+ "NOTICE"
30
32
  ],
31
33
  "keywords": [
32
34
  "mcp",
@@ -0,0 +1,137 @@
1
+ import { createHash } from "node:crypto";
2
+ import {
3
+ lstatSync,
4
+ mkdirSync,
5
+ mkdtempSync,
6
+ readFileSync,
7
+ realpathSync,
8
+ renameSync,
9
+ rmSync,
10
+ writeFileSync,
11
+ } from "node:fs";
12
+ import { dirname, join, relative } from "node:path";
13
+
14
+ const LIMIT = 256 * 1024;
15
+ export const MEMORY_RUNTIME_FILES = [
16
+ "execute-turn.mjs",
17
+ "hook-config.mjs",
18
+ "hook-events.mjs",
19
+ "local.mjs",
20
+ "memory-api.mjs",
21
+ "pharaoh-memory-hook.mjs",
22
+ "pharaoh-memory.mjs",
23
+ "user-target.mjs",
24
+ "runtime/credentials.js",
25
+ "runtime/package.json",
26
+ "runtime/refresh.js",
27
+ ];
28
+
29
+ function digest(files) {
30
+ return createHash("sha256").update(JSON.stringify(files)).digest("hex");
31
+ }
32
+
33
+ export function privateMemoryPath(path, directory) {
34
+ const stat = lstatSync(path);
35
+ if (
36
+ (directory ? !stat.isDirectory() : !stat.isFile()) ||
37
+ stat.uid !== process.getuid?.() ||
38
+ (stat.mode & 0o077) !== 0
39
+ ) {
40
+ throw new Error("Project memory needs a regular owner-private bundle path.");
41
+ }
42
+ return stat;
43
+ }
44
+
45
+ /** Read only the executable closure from the installed npm package. */
46
+ export function packagedMemoryBundle(packageRoot = new URL("../", import.meta.url)) {
47
+ const files = MEMORY_RUNTIME_FILES.map((name) => {
48
+ let bytes;
49
+ if (name === "runtime/package.json") {
50
+ bytes = Buffer.from('{"type":"module"}\n');
51
+ } else {
52
+ const location = name.startsWith("runtime/")
53
+ ? new URL(`dist/${name.slice(8)}`, packageRoot)
54
+ : new URL(`project-memory/${name}`, packageRoot);
55
+ try {
56
+ bytes = readFileSync(location);
57
+ } catch {
58
+ throw new Error("The Pharaoh package is missing its project memory runtime.");
59
+ }
60
+ if (name === "runtime/refresh.js" && !bytes.includes("function originBoundRefresh(")) {
61
+ throw new Error("The packaged refresh helper is incompatible with project memory.");
62
+ }
63
+ }
64
+ return [name, bytes.toString("base64")];
65
+ });
66
+ const sha256 = digest(files);
67
+ if (Buffer.byteLength(JSON.stringify(files)) >= LIMIT)
68
+ throw new Error("Project memory bundle is oversized.");
69
+ return { sha256, files };
70
+ }
71
+
72
+ export function installMemoryBundle(root, target, bundle) {
73
+ // Check the nearest existing parent before creating anything, including a
74
+ // requested path whose parent is a symlink into the checkout.
75
+ let parent = root;
76
+ for (;;) {
77
+ try {
78
+ const resolved = join(realpathSync(parent), relative(parent, root));
79
+ const within = target ? relative(target, resolved) : "..";
80
+ if (!within || (!within.startsWith("../") && within !== "..")) {
81
+ throw new Error("Install the project memory bundle outside the enrolled checkout.");
82
+ }
83
+ break;
84
+ } catch (error) {
85
+ if (error.code !== "ENOENT" || parent === dirname(parent)) throw error;
86
+ parent = dirname(parent);
87
+ }
88
+ }
89
+ mkdirSync(root, { recursive: true, mode: 0o700 });
90
+ privateMemoryPath(root, true);
91
+ const destination = join(realpathSync(root), bundle.sha256);
92
+ function verifyExisting() {
93
+ try {
94
+ privateMemoryPath(destination, true);
95
+ } catch (error) {
96
+ if (error.code === "ENOENT") return false;
97
+ throw error;
98
+ }
99
+ privateMemoryPath(join(destination, "runtime"), true);
100
+ for (const [name, content] of bundle.files) {
101
+ const path = join(destination, name);
102
+ if (
103
+ privateMemoryPath(path, false).size > LIMIT ||
104
+ readFileSync(path).toString("base64") !== content
105
+ ) {
106
+ throw new Error(
107
+ "Installed project memory bundle was modified. Remove it before reinstalling.",
108
+ );
109
+ }
110
+ }
111
+ return true;
112
+ }
113
+ if (verifyExisting()) return destination;
114
+ const staging = mkdtempSync(join(root, ".memory-client-"));
115
+ try {
116
+ mkdirSync(join(staging, "runtime"), { mode: 0o700 });
117
+ for (const [name, content] of bundle.files) {
118
+ writeFileSync(join(staging, name), Buffer.from(content, "base64"), {
119
+ flag: "wx",
120
+ mode: 0o600,
121
+ });
122
+ }
123
+ try {
124
+ renameSync(staging, destination);
125
+ } catch (error) {
126
+ if (!["EEXIST", "ENOTEMPTY"].includes(error.code) || !verifyExisting()) throw error;
127
+ }
128
+ } finally {
129
+ rmSync(staging, { recursive: true, force: true });
130
+ }
131
+ return destination;
132
+ }
133
+
134
+ /** Derive a hook executable path from the reviewed installation, never from cwd. */
135
+ export function bundledHook(root) {
136
+ return join(root, "pharaoh-memory-hook.mjs");
137
+ }
@@ -0,0 +1,269 @@
1
+ import { git, visibleGitIndex } from "./local.mjs";
2
+ import { PilotError, requestMemory } from "./memory-api.mjs";
3
+ import { projectMemorySession } from "./user-target.mjs";
4
+
5
+ const SHA = /^[a-f0-9]{40}$/i;
6
+ const UUID = /^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i;
7
+ const TERMS = /^[\p{L}\p{N}._+@ $/#:-]+$/u;
8
+ const SECRET =
9
+ /\b(?:[a-f0-9]{32,}|ph(?:at|rt)_[a-z0-9_-]{12,})\b|:\/\/|\b[\w.-]+@[a-z0-9.-]+\.[a-z]{2,}\b/i;
10
+ // Apply the hosted recap safety rules before any agent-written text hits disk.
11
+ const UNSAFE_RECAP = [
12
+ /(?:phat|phrt|pss|dev)_[a-f\d]{64}/i,
13
+ /\x60{3}|~~~|-----BEGIN [A-Z ]*PRIVATE KEY-----/i,
14
+ /\b(?:gh[pousr]_[A-Za-z0-9]{16,}|github_pat_[A-Za-z0-9_]{16,}|sk-(?:proj-)?[A-Za-z0-9_-]{20,}|(?:AKIA|ASIA)[A-Z0-9]{16})\b/i,
15
+ /\bBearer[ \t]+[A-Za-z0-9_.~+/=-]{12,}|\b(?:password|secret|api[_ -]?key|access[_ -]?token)\s*[:=]\s*\S{6,}/i,
16
+ /^\s*(?:assistant|user|system|tool)\s*:/im,
17
+ /^\s*(?:import\s.+\sfrom\s+["']|(?:const|let|var)\s+\w+\s*=|(?:export\s+)?(?:async\s+)?function\s+\w+\s*\(|def\s+\w+\s*\()/m,
18
+ /\p{Bidi_Control}/u,
19
+ ];
20
+ const REF_PATH = /^[\p{L}\p{N}._+@ -]+(?:\/[\p{L}\p{N}._+@ -]+)*$/u;
21
+
22
+ function safeRecap(text) {
23
+ return (
24
+ ![...text].some((char) => {
25
+ const code = char.charCodeAt(0);
26
+ return (code < 32 && code !== 9 && code !== 10 && code !== 13) || code === 127;
27
+ }) && !UNSAFE_RECAP.some((pattern) => pattern.test(text))
28
+ );
29
+ }
30
+
31
+ function hasKeys(input, allowed) {
32
+ return (
33
+ input &&
34
+ typeof input === "object" &&
35
+ !Array.isArray(input) &&
36
+ Object.keys(input).every((key) => allowed.includes(key))
37
+ );
38
+ }
39
+
40
+ function normalizeTerm(value) {
41
+ if (typeof value !== "string" || value.length > 100 || /[\r\n]/.test(value)) return;
42
+ const term = value.replace(/^[ \t]+|[ \t]+$/g, "").replace(/[ \t]+/g, " ");
43
+ if (
44
+ term &&
45
+ term.split(" ").length <= 8 &&
46
+ TERMS.test(term) &&
47
+ !SECRET.test(term) &&
48
+ safeRecap(term)
49
+ ) {
50
+ return term;
51
+ }
52
+ }
53
+
54
+ function recallTerms(input) {
55
+ if (
56
+ hasKeys(input, ["terms"]) &&
57
+ Array.isArray(input.terms) &&
58
+ input.terms.length >= 1 &&
59
+ input.terms.length <= 5
60
+ ) {
61
+ const terms = input.terms.map(normalizeTerm);
62
+ if (terms.every(Boolean)) return [...new Set(terms)];
63
+ }
64
+ throw new PilotError(
65
+ "Use JSON with 1–5 short, non-sensitive keywords, phrases, paths, or symbols in terms.",
66
+ );
67
+ }
68
+
69
+ function validRef(ref) {
70
+ return (
71
+ hasKeys(ref, ["path", "symbol", "pullRequest", "commit"]) &&
72
+ Object.keys(ref).length > 0 &&
73
+ (ref.path === undefined ||
74
+ (typeof ref.path === "string" &&
75
+ ref.path.length <= 180 &&
76
+ REF_PATH.test(ref.path) &&
77
+ !ref.path.split("/").some((part) => part === "." || part === "..") &&
78
+ safeRecap(ref.path))) &&
79
+ (ref.symbol === undefined ||
80
+ (typeof ref.symbol === "string" &&
81
+ ref.symbol.length <= 100 &&
82
+ /^[\w$][\w$.#:-]*$/u.test(ref.symbol) &&
83
+ safeRecap(ref.symbol))) &&
84
+ (ref.pullRequest === undefined ||
85
+ (Number.isInteger(ref.pullRequest) &&
86
+ ref.pullRequest > 0 &&
87
+ ref.pullRequest <= 1_000_000_000)) &&
88
+ (ref.commit === undefined || (typeof ref.commit === "string" && SHA.test(ref.commit)))
89
+ );
90
+ }
91
+
92
+ function validRecap(text, limit) {
93
+ return (
94
+ typeof text === "string" && text.trim().length > 0 && text.length <= limit && safeRecap(text)
95
+ );
96
+ }
97
+
98
+ function captureDraft(input) {
99
+ if (
100
+ !hasKeys(input, ["kind", "summary", "detail", "refs", "verification"]) ||
101
+ !["observation", "lesson", "decision"].includes(input.kind) ||
102
+ !validRecap(input.summary, 500) ||
103
+ (input.detail !== undefined && !validRecap(input.detail, 1000)) ||
104
+ (input.verification !== undefined && !validRecap(input.verification, 300)) ||
105
+ (input.refs !== undefined &&
106
+ (!Array.isArray(input.refs) || input.refs.length > 6 || !input.refs.every(validRef)))
107
+ ) {
108
+ throw new PilotError(
109
+ "Use a JSON candidate with kind, summary (500 characters), optional detail (1,000), verification (300), and up to six refs.",
110
+ );
111
+ }
112
+ return input;
113
+ }
114
+
115
+ function worktreeEvidence(root, rev) {
116
+ try {
117
+ if (git(root, "status", "--porcelain=v1", "--untracked-files=normal")) return true;
118
+ return !git(
119
+ root,
120
+ "for-each-ref",
121
+ `--contains=${rev}`,
122
+ "--count=1",
123
+ "--format=%(objectname)",
124
+ "refs/remotes/origin/",
125
+ );
126
+ } catch {
127
+ return true;
128
+ }
129
+ }
130
+
131
+ function pathAtRevision(root, rev, path) {
132
+ try {
133
+ git(root, "cat-file", "-e", `${rev}:${path}`);
134
+ return true;
135
+ } catch {
136
+ return false;
137
+ }
138
+ }
139
+
140
+ async function citeRefs(root, rev, refs) {
141
+ const unverified = worktreeEvidence(root, rev);
142
+ const needsFullIndex = refs.some((ref) => !ref.commit && !ref.path && ref.symbol);
143
+ const fullIndex = !unverified && needsFullIndex && visibleGitIndex(root);
144
+ return Promise.all(
145
+ refs.map(async (ref) => {
146
+ if (ref.commit || !(ref.path || ref.symbol)) return ref;
147
+ const visible =
148
+ !unverified && (await (ref.path ? visibleGitIndex(root, ref.path) : fullIndex));
149
+ if (!visible || (ref.path && !pathAtRevision(root, rev, ref.path)))
150
+ return { ...ref, provenance: "worktree" };
151
+ return ref;
152
+ }),
153
+ );
154
+ }
155
+
156
+ function validPair(id, version) {
157
+ return (
158
+ typeof id === "string" &&
159
+ UUID.test(id) &&
160
+ Number.isInteger(version) &&
161
+ version > 0 &&
162
+ version <= 1_000_000
163
+ );
164
+ }
165
+
166
+ export async function executeTurn(
167
+ operation,
168
+ token,
169
+ { target, store, read, request = requestMemory, compact = true, loadCaptureCredentials },
170
+ ) {
171
+ const change = (from, to) => {
172
+ const state = store.transition(token, target, from, to);
173
+ if (!state) throw new PilotError("This project memory turn is stale or not eligible.");
174
+ return state;
175
+ };
176
+ if (operation === "receipt") {
177
+ const receipt = store.readReceipt(token, target);
178
+ if (!receipt)
179
+ throw new PilotError(
180
+ "No confirmed capture receipt is available for this turn. Check Project Memory before retrying.",
181
+ );
182
+ return receipt;
183
+ }
184
+ let input;
185
+ try {
186
+ input = await read();
187
+ } catch {
188
+ throw new PilotError("Provide a bounded JSON object on standard input.");
189
+ }
190
+ if (operation === "expand") {
191
+ if (!hasKeys(input, ["id", "version"]) || !validPair(input.id, input.version))
192
+ throw new PilotError("Provide the exact record ID and version from the compact packet.");
193
+ const expanding = store.beginExpand(token, target, input.id, input.version);
194
+ if (!expanding)
195
+ throw new PilotError(
196
+ "This record version is outside the active packet, the turn is stale, or its two expansions are used.",
197
+ );
198
+ let result;
199
+ try {
200
+ result = await request(operation, {
201
+ repo: expanding.repo,
202
+ id: input.id,
203
+ version: input.version,
204
+ });
205
+ } catch (error) {
206
+ store.finishExpand(token, target, expanding.attempt);
207
+ throw error;
208
+ }
209
+ if (!store.finishExpand(token, target, expanding.attempt))
210
+ throw new PilotError("This project memory turn ended before its expansion returned.");
211
+ return result;
212
+ }
213
+ const capture = operation === "capture";
214
+ if (!capture && operation !== "recall") throw new PilotError("Unknown project memory operation.");
215
+ const draft = capture ? captureDraft(input) : { terms: recallTerms(input) };
216
+ if (JSON.stringify(draft).includes(token))
217
+ throw new PilotError("The local turn token cannot appear in a memory request.");
218
+ if (capture) {
219
+ const gitRev = git(target.root, "rev-parse", "HEAD");
220
+ if (!SHA.test(gitRev)) throw new PilotError("The checkout has no full Git commit to cite.");
221
+ const refs = draft.refs ? await citeRefs(target.root, gitRev, draft.refs) : undefined;
222
+ const credentials = await loadCaptureCredentials?.();
223
+ const session = projectMemorySession(credentials);
224
+ if (!session || !(Date.parse(credentials.expires_at) > Date.now()))
225
+ throw new PilotError("Reconnect to Pharaoh to save project memory.");
226
+ const dispatched = store.startCapture(token, target, session, (identity) =>
227
+ request("capture", { ...draft, gitRev, refs, ...identity }, { credentials }),
228
+ );
229
+ if (!dispatched)
230
+ throw new PilotError("This turn is stale, read-only, or has already attempted a capture.");
231
+ let record;
232
+ try {
233
+ ({ record } = await dispatched);
234
+ } catch (error) {
235
+ if (error instanceof PilotError && error.rejected)
236
+ store.saveReceipt(token, target, "rejected");
237
+ throw error;
238
+ }
239
+ try {
240
+ store.saveReceipt(token, target, "confirmed", record);
241
+ } catch {
242
+ throw new PilotError("Pharaoh saved the candidate. Check Project Memory for the record.");
243
+ }
244
+ return;
245
+ }
246
+ const state = change(["pending"], "recalling");
247
+ const payload = {
248
+ repo: state.repo,
249
+ ...draft,
250
+ ...(compact && { format: "compact-v1" }),
251
+ };
252
+ let result;
253
+ try {
254
+ result = await request(operation, payload);
255
+ } catch (error) {
256
+ store.transition(token, target, ["recalling"], "failed");
257
+ throw error;
258
+ }
259
+ const pairs =
260
+ result?.format === "compact-v1" ? result.records.map(({ id, version }) => [id, version]) : [];
261
+ if (
262
+ pairs.length > 5 ||
263
+ !pairs.every(([id, version]) => validPair(id, version)) ||
264
+ !store.finishRecall(token, target, pairs)
265
+ ) {
266
+ throw new PilotError("This project memory turn ended before recall returned.");
267
+ }
268
+ return result;
269
+ }