alp-code 0.9.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.
Files changed (204) hide show
  1. package/CHANGELOG.md +770 -0
  2. package/LICENSE +21 -0
  3. package/README.md +295 -0
  4. package/alp.config.yaml +5 -0
  5. package/dist/src/agents/agent-definition.js +28 -0
  6. package/dist/src/agents/capability-catalog.js +33 -0
  7. package/dist/src/agents/compaction.js +36 -0
  8. package/dist/src/agents/errors.js +12 -0
  9. package/dist/src/agents/librarian.js +38 -0
  10. package/dist/src/agents/main.js +37 -0
  11. package/dist/src/agents/memory-grant.js +29 -0
  12. package/dist/src/agents/model-context.js +70 -0
  13. package/dist/src/agents/modes.js +134 -0
  14. package/dist/src/agents/oracle.js +36 -0
  15. package/dist/src/agents/read-thread.js +38 -0
  16. package/dist/src/agents/registry.js +238 -0
  17. package/dist/src/agents/render-identity.js +38 -0
  18. package/dist/src/agents/review.js +37 -0
  19. package/dist/src/agents/search.js +37 -0
  20. package/dist/src/agents/shared/house-rules.js +33 -0
  21. package/dist/src/agents/shared/principal.js +18 -0
  22. package/dist/src/agents/shared/voice.js +29 -0
  23. package/dist/src/agents/titling.js +32 -0
  24. package/dist/src/agents/types.js +15 -0
  25. package/dist/src/backend/execution-backend.js +2 -0
  26. package/dist/src/backend/local-execution-store.js +144 -0
  27. package/dist/src/backend/local-process-backend.js +533 -0
  28. package/dist/src/backend/local-supervisor.js +104 -0
  29. package/dist/src/cli/alp.js +380 -0
  30. package/dist/src/cli/commands/context.js +203 -0
  31. package/dist/src/cli/commands/delegate.js +136 -0
  32. package/dist/src/cli/commands/identity-sync.js +31 -0
  33. package/dist/src/cli/commands/init.js +184 -0
  34. package/dist/src/cli/commands/mode.js +22 -0
  35. package/dist/src/cli/commands/principal.js +114 -0
  36. package/dist/src/cli/commands/run-main.js +90 -0
  37. package/dist/src/cli/commands/runtime.js +21 -0
  38. package/dist/src/cli/mode-preference-store.js +62 -0
  39. package/dist/src/cli/mode-selector.js +178 -0
  40. package/dist/src/cli/update-check.js +77 -0
  41. package/dist/src/context/checkpoint.js +134 -0
  42. package/dist/src/context/compact-journal.js +153 -0
  43. package/dist/src/context/compact-payload.js +121 -0
  44. package/dist/src/context/continuity.js +70 -0
  45. package/dist/src/context/types.js +2 -0
  46. package/dist/src/delegation/backend-registry.js +40 -0
  47. package/dist/src/delegation/delegation-service.js +300 -0
  48. package/dist/src/delegation/types.js +12 -0
  49. package/dist/src/execution/execution-policy.js +96 -0
  50. package/dist/src/execution/execution-service.js +115 -0
  51. package/dist/src/execution/execution-store.js +78 -0
  52. package/dist/src/execution/identity-capsule.js +65 -0
  53. package/dist/src/execution/types.js +12 -0
  54. package/dist/src/hooks/execution-bridge.js +84 -0
  55. package/dist/src/index.js +4 -0
  56. package/dist/src/memory/adapters/markdown-file-store.js +257 -0
  57. package/dist/src/memory/adapters/memory-api-client.js +2 -0
  58. package/dist/src/memory/adapters/memory-path-mapper.js +76 -0
  59. package/dist/src/memory/adapters/remote-api-store.js +25 -0
  60. package/dist/src/memory/context-ranker.js +21 -0
  61. package/dist/src/memory/errors.js +58 -0
  62. package/dist/src/memory/memory-service.js +149 -0
  63. package/dist/src/memory/memory-store.js +2 -0
  64. package/dist/src/memory/types.js +2 -0
  65. package/dist/src/policy/capability-policy.js +29 -0
  66. package/dist/src/policy/delegation-policy.js +25 -0
  67. package/dist/src/policy/errors.js +10 -0
  68. package/dist/src/policy/invariants.js +31 -0
  69. package/dist/src/policy/memory-policy.js +22 -0
  70. package/dist/src/policy/policy-engine.js +85 -0
  71. package/dist/src/policy/types.js +8 -0
  72. package/dist/src/policy/workspace-policy.js +77 -0
  73. package/dist/src/principal/principal-profile-store.js +89 -0
  74. package/dist/src/runtime/adapter-files.js +147 -0
  75. package/dist/src/runtime/claude-adapter.js +177 -0
  76. package/dist/src/runtime/codex-adapter.js +169 -0
  77. package/dist/src/runtime/permission-rules.js +156 -0
  78. package/dist/src/runtime/render-session-context.js +124 -0
  79. package/dist/src/runtime/render-task-input.js +33 -0
  80. package/dist/src/runtime/runtime-adapter.js +2 -0
  81. package/dist/src/runtime/runtime-preference-store.js +66 -0
  82. package/dist/src/runtime/runtime-selector.js +178 -0
  83. package/dist/src/runtime/types.js +2 -0
  84. package/dist/src/runtime/windows-shim.js +57 -0
  85. package/dist/src/state-paths.js +49 -0
  86. package/dist/src/workflow/output-validator.js +27 -0
  87. package/dist/src/workflow/repair-policy.js +8 -0
  88. package/dist/src/workflow/types.js +22 -0
  89. package/dist/src/workflow/workflow-runner.js +81 -0
  90. package/hooks/compact-record.cjs +109 -0
  91. package/hooks/session-boot.cjs +112 -0
  92. package/hooks/session-end.cjs +34 -0
  93. package/package.json +48 -0
  94. package/scaffold/memory/INDEX.md +27 -0
  95. package/scaffold/memory/README.md +76 -0
  96. package/scaffold/memory/projects/INDEX.md +22 -0
  97. package/scaffold/memory/projects/PROTOCOL.md +128 -0
  98. package/scaffold/memory/projects/_template/PROJECT.md +45 -0
  99. package/scripts/alp.cjs +126 -0
  100. package/scripts/alp.ps1 +4 -0
  101. package/scripts/alp.sh +3 -0
  102. package/scripts/bootstrap.cjs +144 -0
  103. package/scripts/checkout-release.cjs +30 -0
  104. package/scripts/delegate.cjs +19 -0
  105. package/scripts/doctor.cjs +158 -0
  106. package/scripts/doctor.sh +3 -0
  107. package/scripts/ensure-state.cjs +22 -0
  108. package/scripts/lib/cli-link.cjs +375 -0
  109. package/scripts/lib/codex-role.cjs +18 -0
  110. package/scripts/lib/delegation/command-runner.cjs +108 -0
  111. package/scripts/lib/delegation/config.cjs +81 -0
  112. package/scripts/lib/install-paths.cjs +154 -0
  113. package/scripts/lib/release-manifest.cjs +42 -0
  114. package/scripts/lib/semver-lite.cjs +20 -0
  115. package/scripts/lib/state.cjs +274 -0
  116. package/scripts/lib/uninstall.cjs +252 -0
  117. package/scripts/lib/update-check-worker.cjs +21 -0
  118. package/scripts/lib/update.cjs +395 -0
  119. package/scripts/run-role.cjs +42 -0
  120. package/scripts/run-role.ps1 +4 -0
  121. package/scripts/run-role.sh +3 -0
  122. package/scripts/sync-project-index.sh +167 -0
  123. package/skills/agent-memory/SKILL.md +109 -0
  124. package/skills/alp-debug/SKILL.md +90 -0
  125. package/skills/alp-debug/references/defense-in-depth.md +118 -0
  126. package/skills/alp-debug/references/investigation-methodology.md +106 -0
  127. package/skills/alp-debug/references/log-and-ci-analysis.md +96 -0
  128. package/skills/alp-debug/references/performance-diagnostics.md +112 -0
  129. package/skills/alp-debug/references/reporting-standards.md +120 -0
  130. package/skills/alp-debug/references/root-cause-tracing.md +134 -0
  131. package/skills/alp-debug/references/systematic-debugging.md +93 -0
  132. package/skills/alp-debug/references/verification.md +86 -0
  133. package/skills/alp-debug/scripts/find-polluter.sh +63 -0
  134. package/skills/alp-debug/scripts/find-polluter.test.md +102 -0
  135. package/skills/alp-plan/SKILL.md +128 -0
  136. package/skills/alp-plan/references/archive-workflow.md +77 -0
  137. package/skills/alp-plan/references/codebase-understanding.md +55 -0
  138. package/skills/alp-plan/references/output-standards.md +96 -0
  139. package/skills/alp-plan/references/plan-organization.md +129 -0
  140. package/skills/alp-plan/references/red-team-personas.md +76 -0
  141. package/skills/alp-plan/references/red-team-workflow.md +81 -0
  142. package/skills/alp-plan/references/research-phase.md +57 -0
  143. package/skills/alp-plan/references/scope-challenge.md +82 -0
  144. package/skills/alp-plan/references/solution-design.md +76 -0
  145. package/skills/alp-plan/references/validate-question-framework.md +89 -0
  146. package/skills/alp-plan/references/validate-workflow.md +83 -0
  147. package/skills/alp-predict/SKILL.md +98 -0
  148. package/skills/alp-scenario/SKILL.md +86 -0
  149. package/skills/code-review/SKILL.md +111 -0
  150. package/skills/code-review/references/code-review-reception.md +114 -0
  151. package/skills/code-review/references/edge-case-scouting.md +78 -0
  152. package/skills/code-review/references/verification-before-completion.md +117 -0
  153. package/skills/delegation/SKILL.md +46 -0
  154. package/skills/docs-seeker/.env.example +15 -0
  155. package/skills/docs-seeker/SKILL.md +87 -0
  156. package/skills/docs-seeker/package.json +25 -0
  157. package/skills/docs-seeker/references/advanced.md +82 -0
  158. package/skills/docs-seeker/references/context7-patterns.md +68 -0
  159. package/skills/docs-seeker/references/errors.md +72 -0
  160. package/skills/docs-seeker/scripts/analyze-llms-txt.js +211 -0
  161. package/skills/docs-seeker/scripts/detect-topic.js +172 -0
  162. package/skills/docs-seeker/scripts/fetch-docs.js +213 -0
  163. package/skills/docs-seeker/scripts/tests/run-tests.js +72 -0
  164. package/skills/docs-seeker/scripts/tests/test-analyze-llms.js +119 -0
  165. package/skills/docs-seeker/scripts/tests/test-detect-topic.js +112 -0
  166. package/skills/docs-seeker/scripts/tests/test-fetch-docs.js +84 -0
  167. package/skills/docs-seeker/scripts/utils/env-loader.js +94 -0
  168. package/skills/docs-seeker/workflows/library-search.md +73 -0
  169. package/skills/docs-seeker/workflows/repo-analysis.md +90 -0
  170. package/skills/docs-seeker/workflows/topic-search.md +69 -0
  171. package/skills/git/SKILL.md +121 -0
  172. package/skills/git/references/branch-management.md +90 -0
  173. package/skills/git/references/commit-standards.md +82 -0
  174. package/skills/git/references/gh-cli-guide.md +132 -0
  175. package/skills/git/references/safety-protocols.md +86 -0
  176. package/skills/git/references/workflow-commit.md +89 -0
  177. package/skills/git/references/workflow-merge.md +63 -0
  178. package/skills/git/references/workflow-pr.md +70 -0
  179. package/skills/git/references/workflow-push.md +62 -0
  180. package/skills/gkg/SKILL.md +87 -0
  181. package/skills/gkg/references/cli-commands.md +92 -0
  182. package/skills/gkg/references/http-api.md +99 -0
  183. package/skills/gkg/references/language-support.md +54 -0
  184. package/skills/problem-solving/SKILL.md +86 -0
  185. package/skills/problem-solving/references/attribution.md +48 -0
  186. package/skills/problem-solving/references/collision-zone-thinking.md +71 -0
  187. package/skills/problem-solving/references/inversion-exercise.md +88 -0
  188. package/skills/problem-solving/references/meta-pattern-recognition.md +80 -0
  189. package/skills/problem-solving/references/scale-game.md +82 -0
  190. package/skills/problem-solving/references/simplification-cascades.md +83 -0
  191. package/skills/problem-solving/references/when-stuck.md +76 -0
  192. package/skills/repomix/SKILL.md +94 -0
  193. package/skills/repomix/references/configuration.md +134 -0
  194. package/skills/repomix/references/usage-patterns.md +106 -0
  195. package/skills/repomix/scripts/.coverage +0 -0
  196. package/skills/repomix/scripts/README.md +179 -0
  197. package/skills/repomix/scripts/repomix_batch.py +455 -0
  198. package/skills/repomix/scripts/repos.example.json +15 -0
  199. package/skills/repomix/scripts/requirements.txt +15 -0
  200. package/skills/repomix/scripts/tests/test_repomix_batch.py +531 -0
  201. package/skills/research/SKILL.md +107 -0
  202. package/skills/security-scan/SKILL.md +101 -0
  203. package/skills/security-scan/references/secret-patterns.md +75 -0
  204. package/skills/security-scan/references/vulnerability-patterns.md +136 -0
@@ -0,0 +1,178 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ModeSelector = void 0;
4
+ const node_stream_1 = require("node:stream");
5
+ const node_readline_1 = require("node:readline");
6
+ const modes_1 = require("../agents/modes");
7
+ const mode_preference_store_1 = require("./mode-preference-store");
8
+ /**
9
+ * Menu chọn **nấc**, không phải chọn runtime.
10
+ *
11
+ * Từ khi mỗi vai ở mỗi nấc chỉ có một model, model quyết định CLI — nên câu hỏi lúc mở phiên
12
+ * không còn là "chạy Claude hay Codex" mà là "việc này khó cỡ nào". Menu vẫn giữ nguyên
13
+ * khuôn cũ: nhớ lựa chọn lần trước, hỏi một lần trên TTY, Ctrl+C là huỷ chứ không phải chọn
14
+ * bừa.
15
+ */
16
+ const MODES = modes_1.MODE_IDS;
17
+ function normalizeKeypress(sequence, key = {}) {
18
+ if (key.ctrl && key.name === "c")
19
+ return "cancel";
20
+ if (key.name === "up")
21
+ return "up";
22
+ if (key.name === "down")
23
+ return "down";
24
+ if (key.name === "return" ||
25
+ key.name === "enter" ||
26
+ sequence === "\r" ||
27
+ sequence === "\n") {
28
+ return "enter";
29
+ }
30
+ return "other";
31
+ }
32
+ function createKeypressReader(input) {
33
+ const decoder = new node_stream_1.PassThrough();
34
+ const queued = [];
35
+ const waiting = [];
36
+ let terminalError = null;
37
+ const onKeypress = (sequence, key) => {
38
+ const value = normalizeKeypress(sequence, key);
39
+ const waiter = waiting.shift();
40
+ if (waiter)
41
+ waiter.resolve(value);
42
+ else
43
+ queued.push(value);
44
+ };
45
+ const onError = (error) => {
46
+ terminalError = error;
47
+ for (const waiter of waiting.splice(0))
48
+ waiter.reject(error);
49
+ };
50
+ const onData = (chunk) => {
51
+ decoder.write(chunk);
52
+ };
53
+ (0, node_readline_1.emitKeypressEvents)(decoder);
54
+ decoder.on("keypress", onKeypress);
55
+ input.on("data", onData);
56
+ input.on("error", onError);
57
+ return {
58
+ read() {
59
+ const queuedKey = queued.shift();
60
+ if (queuedKey !== undefined)
61
+ return Promise.resolve(queuedKey);
62
+ if (terminalError !== null)
63
+ return Promise.reject(terminalError);
64
+ return new Promise((resolve, reject) => {
65
+ waiting.push({ resolve, reject });
66
+ });
67
+ },
68
+ close() {
69
+ input.removeListener("data", onData);
70
+ input.removeListener("error", onError);
71
+ decoder.removeListener("keypress", onKeypress);
72
+ decoder.destroy();
73
+ },
74
+ };
75
+ }
76
+ function renderMenu(output, current, selectedIndex, redraw) {
77
+ if (redraw)
78
+ output.write(`\u001b[${MODES.length + 1}A`);
79
+ for (const [index, mode] of MODES.entries()) {
80
+ const pointer = index === selectedIndex ? "❯" : " ";
81
+ const persisted = mode === current ? " (current)" : "";
82
+ output.write(`\r\u001b[2K ${pointer} ${mode.padEnd(6)}${persisted} — ${modes_1.MODE_PROFILES[mode].summary}\n`);
83
+ }
84
+ output.write("\r\u001b[2K↑/↓ select · Enter confirm · Ctrl+C cancel\n");
85
+ }
86
+ class ModeSelector {
87
+ preferenceStore;
88
+ input;
89
+ output;
90
+ injectedReadKey;
91
+ constructor(options = {}) {
92
+ this.preferenceStore = options.preferenceStore ?? new mode_preference_store_1.FileModePreferenceStore();
93
+ this.input = options.input ?? process.stdin;
94
+ this.output = options.output ?? process.stdout;
95
+ this.injectedReadKey = options.readKey;
96
+ }
97
+ async select(input) {
98
+ if (input.requestedMode !== undefined) {
99
+ if (!MODES.includes(input.requestedMode)) {
100
+ throw new Error(`invalid mode \`${String(input.requestedMode)}\``);
101
+ }
102
+ return { ok: true, mode: input.requestedMode, source: "explicit" };
103
+ }
104
+ const preference = await this.preferenceStore.read();
105
+ if (preference.warning) {
106
+ this.output.write(`WARNING ${preference.warning}\n`);
107
+ }
108
+ const current = preference.mode ?? modes_1.DEFAULT_MODE;
109
+ if (!input.interactive) {
110
+ return {
111
+ ok: true,
112
+ mode: current,
113
+ source: preference.mode === null ? "default" : "persisted",
114
+ };
115
+ }
116
+ const selected = await this.prompt(current);
117
+ if (selected === null)
118
+ return { ok: false, exitCode: 130 };
119
+ await this.preferenceStore.write(selected);
120
+ return { ok: true, mode: selected, source: "interactive" };
121
+ }
122
+ async prompt(current) {
123
+ if (!this.injectedReadKey && !this.input) {
124
+ throw new Error("interactive mode selection requires terminal input");
125
+ }
126
+ const wasFlowing = this.input?.readableFlowing === true;
127
+ const wasRaw = Boolean(this.input?.isRaw);
128
+ const keypress = this.injectedReadKey || !this.input
129
+ ? null
130
+ : createKeypressReader(this.input);
131
+ const readKey = this.injectedReadKey ?? keypress.read;
132
+ let changedRawMode = false;
133
+ let selectedIndex = Math.max(0, MODES.indexOf(current));
134
+ this.output.write("\nSelect mode for this ALP session:\n");
135
+ this.output.write("\u001b[?25l");
136
+ try {
137
+ if (keypress &&
138
+ !wasRaw &&
139
+ typeof this.input?.setRawMode === "function") {
140
+ this.input.setRawMode(true);
141
+ changedRawMode = true;
142
+ }
143
+ if (keypress && typeof this.input?.resume === "function") {
144
+ this.input.resume();
145
+ }
146
+ renderMenu(this.output, current, selectedIndex, false);
147
+ for (;;) {
148
+ const key = await readKey();
149
+ if (key === "up") {
150
+ selectedIndex = (selectedIndex - 1 + MODES.length) % MODES.length;
151
+ renderMenu(this.output, current, selectedIndex, true);
152
+ }
153
+ else if (key === "down") {
154
+ selectedIndex = (selectedIndex + 1) % MODES.length;
155
+ renderMenu(this.output, current, selectedIndex, true);
156
+ }
157
+ else if (key === "enter") {
158
+ return MODES[selectedIndex];
159
+ }
160
+ else if (key === "cancel") {
161
+ return null;
162
+ }
163
+ }
164
+ }
165
+ finally {
166
+ keypress?.close();
167
+ if (keypress &&
168
+ !wasFlowing &&
169
+ typeof this.input?.pause === "function") {
170
+ this.input.pause();
171
+ }
172
+ if (changedRawMode)
173
+ this.input?.setRawMode?.(false);
174
+ this.output.write("\u001b[?25h");
175
+ }
176
+ }
177
+ }
178
+ exports.ModeSelector = ModeSelector;
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FileUpdateCheckStore = void 0;
4
+ exports.spawnBackgroundUpdateCheck = spawnBackgroundUpdateCheck;
5
+ exports.checkForUpdate = checkForUpdate;
6
+ const node_child_process_1 = require("node:child_process");
7
+ const node_crypto_1 = require("node:crypto");
8
+ const node_module_1 = require("node:module");
9
+ const node_path_1 = require("node:path");
10
+ const promises_1 = require("node:fs/promises");
11
+ const state_paths_1 = require("../state-paths");
12
+ const TTL_MS = 24 * 60 * 60 * 1000;
13
+ class FileUpdateCheckStore {
14
+ file;
15
+ constructor(options = {}) {
16
+ this.file = options.file ?? (0, node_path_1.join)((0, state_paths_1.stateHome)(), "update-check.json");
17
+ }
18
+ async read() {
19
+ try {
20
+ const parsed = JSON.parse(await (0, promises_1.readFile)(this.file, "utf8"));
21
+ return typeof parsed?.checkedAt === "number"
22
+ ? { checkedAt: parsed.checkedAt, latestTag: typeof parsed.latestTag === "string" ? parsed.latestTag : null }
23
+ : null;
24
+ }
25
+ catch {
26
+ return null;
27
+ }
28
+ }
29
+ async write(cache) {
30
+ const dir = (0, node_path_1.dirname)(this.file);
31
+ await (0, promises_1.mkdir)(dir, { recursive: true, mode: 0o700 });
32
+ const tmp = (0, node_path_1.join)(dir, `.${(0, node_crypto_1.randomUUID)()}.update-check.tmp`);
33
+ try {
34
+ await (0, promises_1.writeFile)(tmp, `${JSON.stringify(cache)}\n`, { encoding: "utf8", flag: "wx", mode: 0o600 });
35
+ await (0, promises_1.rename)(tmp, this.file);
36
+ }
37
+ catch (error) {
38
+ await (0, promises_1.rm)(tmp, { force: true });
39
+ throw error;
40
+ }
41
+ }
42
+ }
43
+ exports.FileUpdateCheckStore = FileUpdateCheckStore;
44
+ function spawnBackgroundUpdateCheck(repoRoot) {
45
+ try {
46
+ const child = (0, node_child_process_1.spawn)(process.execPath, [(0, node_path_1.join)(repoRoot, "scripts", "lib", "update-check-worker.cjs")], {
47
+ cwd: repoRoot,
48
+ detached: true,
49
+ stdio: "ignore",
50
+ env: process.env,
51
+ });
52
+ child.unref();
53
+ }
54
+ catch {
55
+ /* best-effort; never affect the current command */
56
+ }
57
+ }
58
+ function isNewer(repoRoot, latestTag, currentVersion) {
59
+ const semver = (0, node_module_1.createRequire)(__filename)((0, node_path_1.join)(repoRoot, "scripts", "lib", "semver-lite.cjs"));
60
+ try {
61
+ return semver.compare(latestTag, currentVersion) > 0;
62
+ }
63
+ catch {
64
+ return false;
65
+ }
66
+ }
67
+ async function checkForUpdate(input) {
68
+ const now = input.now ?? Date.now();
69
+ const cached = await input.store.read();
70
+ if (!cached || now - cached.checkedAt >= TTL_MS)
71
+ (input.triggerBackgroundRefresh ?? spawnBackgroundUpdateCheck)(input.repoRoot);
72
+ if (!cached?.latestTag)
73
+ return null;
74
+ if (!isNewer(input.repoRoot, cached.latestTag, input.currentVersion))
75
+ return null;
76
+ return `UPDATE phiên bản mới ${cached.latestTag} hiện có (đang dùng v${input.currentVersion}) — chạy \`alp update\`\n`;
77
+ }
@@ -0,0 +1,134 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CHECKPOINT_MAX_BYTES = exports.PIN_MAX_BYTES = void 0;
4
+ exports.checkpointDigest = checkpointDigest;
5
+ exports.seedCheckpoint = seedCheckpoint;
6
+ exports.readCheckpoint = readCheckpoint;
7
+ exports.writeCheckpoint = writeCheckpoint;
8
+ const node_crypto_1 = require("node:crypto");
9
+ const promises_1 = require("node:fs/promises");
10
+ const zod_1 = require("zod");
11
+ const adapter_files_1 = require("../runtime/adapter-files");
12
+ /** §6 Size limits — one pin, and the checkpoint as a whole, in UTF-8 bytes. */
13
+ exports.PIN_MAX_BYTES = 4 * 1024;
14
+ exports.CHECKPOINT_MAX_BYTES = 128 * 1024;
15
+ const EXECUTION_ID_PATTERN = /^exec_[a-zA-Z0-9_-]+$/;
16
+ function byteLength(value) {
17
+ return Buffer.byteLength(value, "utf8");
18
+ }
19
+ const continuityPinSchema = zod_1.z.object({
20
+ id: zod_1.z.string().min(1),
21
+ text: zod_1.z.string().min(1).refine((value) => byteLength(value) <= exports.PIN_MAX_BYTES, {
22
+ message: `pin text exceeds ${exports.PIN_MAX_BYTES} bytes`,
23
+ }),
24
+ source: zod_1.z.enum(["execution", "principal", "agent"]),
25
+ createdAt: zod_1.z.string().min(1),
26
+ });
27
+ const checkpointSchema = zod_1.z.object({
28
+ version: zod_1.z.literal(1),
29
+ executionId: zod_1.z.string().regex(EXECUTION_ID_PATTERN, "invalid execution ID"),
30
+ policyHash: zod_1.z.string().min(1),
31
+ runtime: zod_1.z.enum(["claude", "codex"]).nullable(),
32
+ createdAt: zod_1.z.string().min(1),
33
+ updatedAt: zod_1.z.string().min(1),
34
+ objective: zod_1.z.string().nullable(),
35
+ decisions: zod_1.z.array(continuityPinSchema),
36
+ constraints: zod_1.z.array(continuityPinSchema),
37
+ openItems: zod_1.z.array(continuityPinSchema),
38
+ nextActions: zod_1.z.array(continuityPinSchema),
39
+ integrity: zod_1.z.object({ checkpointSha256: zod_1.z.string().length(64) }),
40
+ }).superRefine((value, ctx) => {
41
+ if (byteLength(JSON.stringify(value)) > exports.CHECKPOINT_MAX_BYTES) {
42
+ ctx.addIssue({ code: "custom", message: `checkpoint exceeds ${exports.CHECKPOINT_MAX_BYTES} bytes` });
43
+ }
44
+ });
45
+ /**
46
+ * Sort object keys recursively so two checkpoints with the same logical content hash the
47
+ * same regardless of the key order they happen to be written or read in. Arrays keep their
48
+ * order — pins are a sequence, not a set.
49
+ */
50
+ function sortForDigest(value) {
51
+ if (Array.isArray(value))
52
+ return value.map(sortForDigest);
53
+ if (value !== null && typeof value === "object") {
54
+ const sorted = {};
55
+ for (const key of Object.keys(value).sort()) {
56
+ sorted[key] = sortForDigest(value[key]);
57
+ }
58
+ return sorted;
59
+ }
60
+ return value;
61
+ }
62
+ /** SHA-256 over the checkpoint's canonical JSON, excluding `integrity` itself. */
63
+ function checkpointDigest(checkpoint) {
64
+ return (0, node_crypto_1.createHash)("sha256").update(JSON.stringify(sortForDigest(checkpoint))).digest("hex");
65
+ }
66
+ /** The checkpoint an execution starts with: no pins, objective seeded from the task. */
67
+ function seedCheckpoint(input) {
68
+ const timestamp = (input.now ?? (() => new Date().toISOString()))();
69
+ const body = {
70
+ version: 1,
71
+ executionId: input.executionId,
72
+ policyHash: input.policyHash,
73
+ runtime: null,
74
+ createdAt: timestamp,
75
+ updatedAt: timestamp,
76
+ objective: input.objective,
77
+ decisions: [],
78
+ constraints: [],
79
+ openItems: [],
80
+ nextActions: [],
81
+ };
82
+ return { ...body, integrity: { checkpointSha256: checkpointDigest(body) } };
83
+ }
84
+ /**
85
+ * Fail closed (invariant 6): a checkpoint that doesn't parse, doesn't hash, or is bound to a
86
+ * different execution or policy is never returned as usable — the caller gets a reason
87
+ * instead, and decides on its own whether that means "inject nothing" or "abort".
88
+ */
89
+ async function readCheckpoint(path, binding) {
90
+ let raw;
91
+ try {
92
+ raw = await (0, promises_1.readFile)(path, "utf8");
93
+ }
94
+ catch (error) {
95
+ return { ok: false, reason: `cannot read checkpoint: ${error.message}` };
96
+ }
97
+ let parsed;
98
+ try {
99
+ parsed = JSON.parse(raw);
100
+ }
101
+ catch {
102
+ return { ok: false, reason: "checkpoint is not valid JSON" };
103
+ }
104
+ const result = checkpointSchema.safeParse(parsed);
105
+ if (!result.success) {
106
+ return { ok: false, reason: `checkpoint failed schema validation: ${result.error.issues[0]?.message ?? "invalid"}` };
107
+ }
108
+ const value = result.data;
109
+ const { integrity, ...rest } = value;
110
+ if (checkpointDigest(rest) !== integrity.checkpointSha256) {
111
+ return { ok: false, reason: "checkpoint digest mismatch" };
112
+ }
113
+ if (value.executionId !== binding.executionId || value.policyHash !== binding.policyHash) {
114
+ return { ok: false, reason: "checkpoint policy binding mismatch" };
115
+ }
116
+ return { ok: true, value };
117
+ }
118
+ /**
119
+ * Validates before touching the filesystem at all, so a rejected write (oversize pin,
120
+ * oversize checkpoint) never runs `atomicRuntimeFile` — the file on disk, if any, is left
121
+ * exactly as it was. A write that does reach `atomicRuntimeFile` is itself temp-file-then-
122
+ * rename, so a process killed mid-write leaves the prior file intact too.
123
+ */
124
+ async function writeCheckpoint(path, checkpoint) {
125
+ // Callers often pass a full `ContinuityCheckpointV1` they already have in hand (e.g. a
126
+ // seeded or previously-read one) — TS only checks excess properties on literals, so a
127
+ // stale `integrity` can arrive here at runtime despite the `Omit` type. Strip it before
128
+ // hashing rather than hash whatever happened to be attached.
129
+ const { integrity: _stale, ...body } = checkpoint;
130
+ const value = { ...body, integrity: { checkpointSha256: checkpointDigest(body) } };
131
+ const parsed = checkpointSchema.parse(value);
132
+ await (0, adapter_files_1.atomicRuntimeFile)(path, `${JSON.stringify(parsed, null, 2)}\n`);
133
+ return parsed;
134
+ }
@@ -0,0 +1,153 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.JOURNAL_ROTATE_BYTES = exports.JOURNAL_LINE_MAX_BYTES = void 0;
4
+ exports.appendCompactJournalLine = appendCompactJournalLine;
5
+ exports.rotateCompactJournalIfNeeded = rotateCompactJournalIfNeeded;
6
+ exports.replayCompactJournal = replayCompactJournal;
7
+ exports.reduceCompactJournal = reduceCompactJournal;
8
+ const promises_1 = require("node:fs/promises");
9
+ const zod_1 = require("zod");
10
+ const compact_payload_1 = require("./compact-payload");
11
+ const EXECUTION_ID_PATTERN = /^exec_[a-zA-Z0-9_-]+$/;
12
+ /** §6 size limits. */
13
+ exports.JOURNAL_LINE_MAX_BYTES = 16 * 1024;
14
+ exports.JOURNAL_ROTATE_BYTES = 1024 * 1024;
15
+ const envelopeSchema = zod_1.z.object({
16
+ v: zod_1.z.literal(1),
17
+ at: zod_1.z.string().min(1),
18
+ executionId: zod_1.z.string().regex(EXECUTION_ID_PATTERN, "invalid execution ID"),
19
+ policyHash: zod_1.z.string().min(1),
20
+ runtime: zod_1.z.enum(["claude", "codex"]),
21
+ phase: zod_1.z.enum(["pre", "post"]),
22
+ source: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()),
23
+ });
24
+ function byteLength(value) {
25
+ return Buffer.byteLength(value, "utf8");
26
+ }
27
+ /**
28
+ * The TS append path used by the CLI and by tests. The hook itself (`compact-record.cjs`)
29
+ * is zero-dependency and appends with a raw `appendFileSync` — this reimplements the same
30
+ * one-syscall, `O_APPEND` shape so both writers produce lines the other can replay, without
31
+ * pulling the hook into the dependency graph this module already has (invariant 4).
32
+ */
33
+ async function appendCompactJournalLine(path, envelope) {
34
+ const line = `${JSON.stringify(envelope)}\n`;
35
+ if (byteLength(line) > exports.JOURNAL_LINE_MAX_BYTES) {
36
+ throw new Error(`journal line exceeds ${exports.JOURNAL_LINE_MAX_BYTES} bytes`);
37
+ }
38
+ await (0, promises_1.appendFile)(path, line, { encoding: "utf8", mode: 0o600, flag: "a" });
39
+ }
40
+ /**
41
+ * Rotates `path` to `path.1` (overwriting any prior `.1`) when it has reached
42
+ * `JOURNAL_ROTATE_BYTES`. Called from `alp context status|validate`, never from the hook —
43
+ * the hook only ever appends (invariant 4).
44
+ */
45
+ async function rotateCompactJournalIfNeeded(path) {
46
+ let size;
47
+ try {
48
+ size = (await (0, promises_1.stat)(path)).size;
49
+ }
50
+ catch {
51
+ return false;
52
+ }
53
+ if (size < exports.JOURNAL_ROTATE_BYTES)
54
+ return false;
55
+ await (0, promises_1.rename)(path, `${path}.1`);
56
+ return true;
57
+ }
58
+ async function readIfExists(path) {
59
+ try {
60
+ return await (0, promises_1.readFile)(path, "utf8");
61
+ }
62
+ catch {
63
+ return null;
64
+ }
65
+ }
66
+ /**
67
+ * Reads `path.1` (if rotation has happened) then `path`, in that order, and turns every
68
+ * well-formed line into a `CompactEventV1`. A line that is not valid JSON, or does not match
69
+ * the envelope schema, is dropped and counted rather than failing the whole replay — a
70
+ * single corrupted append (e.g. a process killed mid-write) must not make the rest of the
71
+ * journal unreadable.
72
+ */
73
+ async function replayCompactJournal(path) {
74
+ const rotated = await readIfExists(`${path}.1`);
75
+ const current = await readIfExists(path);
76
+ const lines = [rotated ?? "", current ?? ""]
77
+ .join("\n")
78
+ .split("\n")
79
+ .map((line) => line.trim())
80
+ .filter((line) => line.length > 0);
81
+ const events = [];
82
+ let droppedLines = 0;
83
+ for (const line of lines) {
84
+ let parsed;
85
+ try {
86
+ parsed = JSON.parse(line);
87
+ }
88
+ catch {
89
+ droppedLines += 1;
90
+ continue;
91
+ }
92
+ const result = envelopeSchema.safeParse(parsed);
93
+ if (!result.success) {
94
+ droppedLines += 1;
95
+ continue;
96
+ }
97
+ const envelope = result.data;
98
+ const normalized = (0, compact_payload_1.normalizeCompactEvent)({
99
+ runtime: envelope.runtime,
100
+ phase: envelope.phase,
101
+ // `normalizeCompactEvent` treats this as unknown input and filters it against its own
102
+ // per-runtime whitelist; the envelope schema only guarantees an object, not that its
103
+ // values are already strings.
104
+ source: envelope.source,
105
+ observedAt: envelope.at,
106
+ });
107
+ if (!normalized.ok) {
108
+ droppedLines += 1;
109
+ continue;
110
+ }
111
+ events.push(normalized.value);
112
+ }
113
+ return { events: Object.freeze(events), droppedLines };
114
+ }
115
+ /** The pairing key a `started`/`completed` pair share — `dedupeKey` without its phase suffix. */
116
+ function pairKeyOf(event) {
117
+ return event.dedupeKey.slice(0, event.dedupeKey.lastIndexOf("|"));
118
+ }
119
+ /**
120
+ * Folds a replayed event list into the state `alp context status` reports. Duplicate lines
121
+ * (same `dedupeKey`, e.g. a hook retried by the runtime) are collapsed before anything else,
122
+ * so `generation` counts distinct completions rather than journal lines.
123
+ *
124
+ * `pending` tracks only the single most recent `started` — a later `started` always
125
+ * supersedes an earlier, still-unmatched one (the orphaned-`PreCompact` case measured on
126
+ * Claude 2026-09-04), and a `completed` only clears it when the pairing key actually
127
+ * matches: a late completion for an older event must not clear a newer pending one.
128
+ */
129
+ function reduceCompactJournal(events) {
130
+ const seenDedupeKeys = new Set();
131
+ let generation = 0;
132
+ let lastCompleted = null;
133
+ let pending = null;
134
+ let pendingPairKey = null;
135
+ for (const event of events) {
136
+ if (seenDedupeKeys.has(event.dedupeKey))
137
+ continue;
138
+ seenDedupeKeys.add(event.dedupeKey);
139
+ const pairKey = pairKeyOf(event);
140
+ if (event.phase === "started") {
141
+ pending = event;
142
+ pendingPairKey = pairKey;
143
+ continue;
144
+ }
145
+ generation += 1;
146
+ lastCompleted = event;
147
+ if (pairKey === pendingPairKey) {
148
+ pending = null;
149
+ pendingPairKey = null;
150
+ }
151
+ }
152
+ return { generation, pending, lastCompleted };
153
+ }
@@ -0,0 +1,121 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.filterCompactSource = filterCompactSource;
4
+ exports.normalizeCompactEvent = normalizeCompactEvent;
5
+ /**
6
+ * Per-runtime whitelist of hook payload keys a journal line may carry.
7
+ *
8
+ * A whitelist, not a blacklist, because a payload is a floor and not a ceiling: the schemas
9
+ * compiled into both binaries under-declare what they actually send. A live Claude
10
+ * `SessionStart` on 2026-09-03 carried `seconds_since_last_response`, `context_tokens`,
11
+ * `prompt_cache_likely_expired` and `estimated_cache_write_usd` that no schema mentions, and
12
+ * carried a different set per `source`. A blacklist would have leaked every one of them into
13
+ * a file ALP keeps.
14
+ *
15
+ * What is deliberately absent, and why:
16
+ * - `compact_summary` (Claude, 22-32 KB measured) — invariant 8. It is provider history the
17
+ * runtime already holds, and one line of it would blow the 16 KiB journal line limit.
18
+ * - `custom_instructions` (Claude `PreCompact`) — free text from the user's own prompt.
19
+ * - `transcript_path`, `cwd` — paths ALP already knows and does not need duplicated per line.
20
+ * - `hook_event_name` — the phase comes from the hook's argv, which is the value ALP
21
+ * controls; trusting the payload's own name for it would let the payload pick its bucket.
22
+ * - `permission_mode` — policy state, owned by ALP, not something to re-learn from a hook.
23
+ */
24
+ const SOURCE_WHITELIST = Object.freeze({
25
+ claude: Object.freeze(["session_id", "trigger", "model", "prompt_id", "agent_id", "agent_type"]),
26
+ codex: Object.freeze(["session_id", "trigger", "model", "turn_id", "agent_id", "agent_type"]),
27
+ });
28
+ /** Per §6 size limits. A whitelisted key with an over-long value is truncated, not dropped. */
29
+ const MAX_VALUE_LENGTH = 256;
30
+ /**
31
+ * Which key carries the runtime's own id for the event.
32
+ *
33
+ * Measured 2026-09-03/04: Claude's `prompt_id` is the same value across `PreCompact`,
34
+ * `SessionStart(source="compact")` and `PostCompact` of one compaction. Codex's `turn_id`
35
+ * behaves differently by launch mode — one id per compaction in an inherited TTY, but a
36
+ * single root-turn id shared by every compaction under `exec` (23 of them in one session).
37
+ * That is why `dedupeKey` prefers an ALP-assigned sequence; see `normalizeCompactEvent`.
38
+ */
39
+ const EVENT_ID_KEY = Object.freeze({
40
+ claude: "prompt_id",
41
+ codex: "turn_id",
42
+ });
43
+ const PHASE_BY_WIRE = Object.freeze({
44
+ pre: "started",
45
+ post: "completed",
46
+ });
47
+ function isRuntimeId(value) {
48
+ return value === "claude" || value === "codex";
49
+ }
50
+ /**
51
+ * Copy the whitelisted scalars out of a raw hook payload.
52
+ *
53
+ * Scalars only: a whitelisted key holding an object or array is dropped rather than
54
+ * stringified, because the point is to keep the line small and predictable, and nothing ALP
55
+ * reads back needs a nested value. Numbers and booleans are kept as their string spelling so
56
+ * a journal line has exactly one value type.
57
+ */
58
+ function filterCompactSource(runtime, payload) {
59
+ if (typeof payload !== "object" || payload === null)
60
+ return Object.freeze({});
61
+ const raw = payload;
62
+ const source = {};
63
+ for (const key of SOURCE_WHITELIST[runtime]) {
64
+ const value = raw[key];
65
+ if (typeof value === "string")
66
+ source[key] = value.slice(0, MAX_VALUE_LENGTH);
67
+ else if (typeof value === "number" || typeof value === "boolean")
68
+ source[key] = String(value);
69
+ }
70
+ return Object.freeze(source);
71
+ }
72
+ function readTrigger(source) {
73
+ const trigger = source.trigger;
74
+ return trigger === "manual" || trigger === "auto" ? trigger : "unknown";
75
+ }
76
+ /**
77
+ * Turn one journal line into a `CompactEventV1`, or say why it cannot be one.
78
+ *
79
+ * Rejects a structurally wrong line — unknown runtime, unknown phase, a `session_id` that is
80
+ * present but not a string — because those mean the line was written by something other than
81
+ * ALP's hook and guessing at it would put fiction in the reduced state. Tolerates a bad
82
+ * `trigger`, which degrades to `"unknown"`: the type has that state precisely so a future
83
+ * trigger value cannot fail an otherwise good replay.
84
+ */
85
+ function normalizeCompactEvent(input) {
86
+ const issues = [];
87
+ if (!isRuntimeId(input.runtime))
88
+ issues.push(`runtime: unknown runtime ${JSON.stringify(input.runtime)}`);
89
+ if (input.phase !== "pre" && input.phase !== "post") {
90
+ issues.push(`phase: expected "pre" or "post", received ${JSON.stringify(input.phase)}`);
91
+ }
92
+ if (typeof input.observedAt !== "string" || input.observedAt.length === 0) {
93
+ issues.push("observedAt: expected a non-empty timestamp");
94
+ }
95
+ if (typeof input.source !== "object" || input.source === null) {
96
+ issues.push("source: expected an object");
97
+ }
98
+ if (issues.length > 0)
99
+ return { ok: false, issues: Object.freeze(issues) };
100
+ const source = filterCompactSource(input.runtime, input.source);
101
+ const runtimeSessionId = source.session_id ?? null;
102
+ const runtimeEventId = source[EVENT_ID_KEY[input.runtime]] ?? null;
103
+ // The compaction's identity, in the order of how well each spelling was measured to
104
+ // separate one compaction from the next. `observedAt` last so that a line missing every
105
+ // id still dedupes against a re-append of itself rather than against a different event.
106
+ const identity = input.sequence === undefined
107
+ ? runtimeEventId ?? input.observedAt
108
+ : String(input.sequence);
109
+ return {
110
+ ok: true,
111
+ value: Object.freeze({
112
+ dedupeKey: [input.runtime, runtimeSessionId ?? "-", identity, PHASE_BY_WIRE[input.phase]].join("|"),
113
+ runtime: input.runtime,
114
+ phase: PHASE_BY_WIRE[input.phase],
115
+ trigger: readTrigger(source),
116
+ runtimeSessionId,
117
+ runtimeEventId,
118
+ observedAt: input.observedAt,
119
+ }),
120
+ };
121
+ }