@superjackfruitlabs/salaam 0.0.4 → 0.0.5

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 (97) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/src/acp-desk.js +140 -0
  3. package/dist/src/acp-outcome.js +23 -19
  4. package/dist/src/acp.js +258 -11
  5. package/dist/src/approval-rules.js +287 -0
  6. package/dist/src/attach.js +3 -1
  7. package/dist/src/autonomy.js +80 -9
  8. package/dist/src/cli-help.js +34 -4
  9. package/dist/src/cli.js +55 -3
  10. package/dist/src/command-danger.js +797 -0
  11. package/dist/src/command-sandbox.js +116 -6
  12. package/dist/src/custom-commands.js +159 -0
  13. package/dist/src/danger-classifier.js +286 -0
  14. package/dist/src/git-helpers.js +160 -0
  15. package/dist/src/hooks.js +351 -0
  16. package/dist/src/host.js +96 -4
  17. package/dist/src/house-extension.js +133 -7
  18. package/dist/src/local-coding.js +553 -58
  19. package/dist/src/local-edits.js +168 -6
  20. package/dist/src/mentions.js +277 -0
  21. package/dist/src/pi-sessions.js +5 -1
  22. package/dist/src/project-instructions.js +195 -0
  23. package/dist/src/protocol.js +4 -1
  24. package/dist/src/service.js +342 -5
  25. package/dist/src/session-pick.js +77 -0
  26. package/dist/src/shell-passthrough.js +39 -0
  27. package/dist/src/store.js +51 -4
  28. package/dist/src/supervisor.js +233 -22
  29. package/dist/src/task-context.js +8 -2
  30. package/dist/src/terminal-status.js +88 -0
  31. package/dist/src/tui-activity.js +99 -0
  32. package/dist/src/tui-classifier.js +25 -0
  33. package/dist/src/tui-client.js +186 -5
  34. package/dist/src/tui-commands.js +23 -5
  35. package/dist/src/tui-composer.js +113 -0
  36. package/dist/src/tui-keymap.js +116 -0
  37. package/dist/src/tui-mentions.js +38 -0
  38. package/dist/src/tui-modals.js +157 -34
  39. package/dist/src/tui-notify.js +62 -0
  40. package/dist/src/tui-review.js +55 -0
  41. package/dist/src/tui-turn-keys.js +52 -0
  42. package/dist/src/tui-view.js +243 -69
  43. package/dist/src/tui.js +875 -118
  44. package/dist/src/turn-checkpoints.js +221 -0
  45. package/dist/src/turn-rewind.js +168 -0
  46. package/dist/src/web-fetch.js +114 -0
  47. package/dist/src/web-guard.js +163 -0
  48. package/dist/src/web-search.js +302 -0
  49. package/npm-shrinkwrap.json +2 -2
  50. package/package.json +1 -1
  51. package/src/acp-desk.ts +155 -0
  52. package/src/acp-outcome.ts +25 -21
  53. package/src/acp.ts +338 -28
  54. package/src/approval-rules.ts +360 -0
  55. package/src/attach.ts +4 -1
  56. package/src/autonomy.ts +111 -10
  57. package/src/cli-help.ts +33 -4
  58. package/src/cli.ts +66 -2
  59. package/src/command-danger.ts +886 -0
  60. package/src/command-sandbox.ts +128 -8
  61. package/src/custom-commands.ts +175 -0
  62. package/src/danger-classifier.ts +321 -0
  63. package/src/git-helpers.ts +177 -0
  64. package/src/hooks.ts +425 -0
  65. package/src/host.ts +122 -1
  66. package/src/house-extension.ts +142 -7
  67. package/src/local-coding.ts +674 -75
  68. package/src/local-edits.ts +191 -8
  69. package/src/mentions.ts +304 -0
  70. package/src/pi-sessions.ts +9 -1
  71. package/src/project-instructions.ts +229 -0
  72. package/src/protocol.ts +19 -2
  73. package/src/service.ts +488 -5
  74. package/src/session-pick.ts +102 -0
  75. package/src/shell-passthrough.ts +38 -0
  76. package/src/store.ts +52 -3
  77. package/src/supervisor.ts +262 -24
  78. package/src/task-context.ts +9 -2
  79. package/src/terminal-status.ts +103 -0
  80. package/src/tui-activity.ts +133 -0
  81. package/src/tui-classifier.ts +38 -0
  82. package/src/tui-client.ts +239 -8
  83. package/src/tui-commands.ts +49 -10
  84. package/src/tui-composer.ts +124 -0
  85. package/src/tui-keymap.ts +152 -0
  86. package/src/tui-mentions.ts +43 -0
  87. package/src/tui-modals.ts +173 -43
  88. package/src/tui-notify.ts +77 -0
  89. package/src/tui-review.ts +76 -0
  90. package/src/tui-turn-keys.ts +77 -0
  91. package/src/tui-view.ts +341 -88
  92. package/src/tui.ts +1047 -137
  93. package/src/turn-checkpoints.ts +253 -0
  94. package/src/turn-rewind.ts +258 -0
  95. package/src/web-fetch.ts +140 -0
  96. package/src/web-guard.ts +195 -0
  97. package/src/web-search.ts +402 -0
@@ -0,0 +1,229 @@
1
+ import { existsSync, lstatSync, readFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { dirname, join, relative, sep } from 'node:path';
4
+ import { canonical, digest, textDigest } from './protocol.js';
5
+ import type { Artifacts } from './artifacts.js';
6
+ import type { Store } from './store.js';
7
+
8
+ /** One file's bound, and the bound on everything loaded together. */
9
+ export const INSTRUCTION_FILE_BYTES = 32 * 1024;
10
+ export const INSTRUCTION_TOTAL_BYTES = 64 * 1024;
11
+ /** AGENTS.md is the shared convention; CLAUDE.md is read only where a directory has no AGENTS.md. */
12
+ const NAMES = ['AGENTS.md', 'CLAUDE.md'];
13
+
14
+ export interface InstructionFile {
15
+ scope: 'user' | 'project';
16
+ /** Shown to people: relative to the session folder, or ~/… for the user file. */
17
+ path: string;
18
+ sha256: string;
19
+ bytes: number;
20
+ text: string;
21
+ }
22
+ export interface InstructionSkip {
23
+ path: string;
24
+ reason: string;
25
+ }
26
+ export interface InstructionsSummary {
27
+ digest: string;
28
+ revision: number;
29
+ files: Omit<InstructionFile, 'text'>[];
30
+ skipped: InstructionSkip[];
31
+ }
32
+ interface InstructionsDocument {
33
+ version: 1;
34
+ files: InstructionFile[];
35
+ skipped: InstructionSkip[];
36
+ }
37
+
38
+ /** The user-level instructions file: $XDG_CONFIG_HOME/salaam/AGENTS.md, else ~/.config/salaam/AGENTS.md. */
39
+ export function userInstructionsPath(env: NodeJS.ProcessEnv = process.env): string {
40
+ return join(env.XDG_CONFIG_HOME || join(homedir(), '.config'), 'salaam', 'AGENTS.md');
41
+ }
42
+
43
+ function repositoryRoot(cwd: string): string | null {
44
+ let dir = cwd;
45
+ for (let i = 0; i < 64; i++) {
46
+ if (existsSync(join(dir, '.git'))) return dir;
47
+ const parent = dirname(dir);
48
+ if (parent === dir) return null;
49
+ dir = parent;
50
+ }
51
+ return null;
52
+ }
53
+
54
+ /**
55
+ * Project instructions for an interactive session, the way other harnesses read them: the
56
+ * user-level file, then AGENTS.md (or CLAUDE.md) in every directory from the repository root down
57
+ * to the session folder. Bounded, regular UTF-8 files only; anything skipped says why.
58
+ */
59
+ export function discoverInstructions(
60
+ cwd: string,
61
+ userFile: string | null = userInstructionsPath(),
62
+ ): { files: InstructionFile[]; skipped: InstructionSkip[] } {
63
+ const files: InstructionFile[] = [];
64
+ const skipped: InstructionSkip[] = [];
65
+ let total = 0;
66
+ /** Returns whether the file exists (so CLAUDE.md is not read beside an AGENTS.md). */
67
+ const take = (scope: InstructionFile['scope'], abs: string, shown: string): boolean => {
68
+ let st;
69
+ try {
70
+ st = lstatSync(abs);
71
+ } catch {
72
+ return false;
73
+ }
74
+ if (!st.isFile()) {
75
+ skipped.push({
76
+ path: shown,
77
+ reason: st.isSymbolicLink() ? 'symbolic links are not loaded' : 'not a regular file',
78
+ });
79
+ return true;
80
+ }
81
+ if (st.size > INSTRUCTION_FILE_BYTES) {
82
+ skipped.push({ path: shown, reason: `larger than ${INSTRUCTION_FILE_BYTES / 1024} KiB` });
83
+ return true;
84
+ }
85
+ const bytes = readFileSync(abs);
86
+ const text = bytes.toString('utf8');
87
+ if (!Buffer.from(text).equals(bytes) || text.includes('\0')) {
88
+ skipped.push({ path: shown, reason: 'not UTF-8 text' });
89
+ return true;
90
+ }
91
+ if (total + bytes.length > INSTRUCTION_TOTAL_BYTES) {
92
+ skipped.push({
93
+ path: shown,
94
+ reason: `instructions together exceed ${INSTRUCTION_TOTAL_BYTES / 1024} KiB`,
95
+ });
96
+ return true;
97
+ }
98
+ total += bytes.length;
99
+ files.push({ scope, path: shown, sha256: textDigest(text), bytes: bytes.length, text });
100
+ return true;
101
+ };
102
+ if (userFile) {
103
+ const home = homedir();
104
+ take(
105
+ 'user',
106
+ userFile,
107
+ userFile.startsWith(home + sep) ? '~/' + relative(home, userFile) : userFile,
108
+ );
109
+ }
110
+ const root = repositoryRoot(cwd) ?? cwd;
111
+ const dirs = [root];
112
+ for (const part of relative(root, cwd).split(sep).filter(Boolean))
113
+ dirs.push(join(dirs.at(-1)!, part));
114
+ for (const dir of dirs)
115
+ for (const name of NAMES) {
116
+ const abs = join(dir, name);
117
+ if (take('project', abs, relative(cwd, abs) || name)) break;
118
+ }
119
+ return { files, skipped };
120
+ }
121
+
122
+ /**
123
+ * Interactive sessions load project instructions automatically and reload them when they change.
124
+ * Each loaded set is kept as an artifact and announced by an `instructions.loaded` event with its
125
+ * digest, so a turn's guidance is traceable to exact bytes. Strict work (board, AgentPod, child
126
+ * workers) never uses this: it keeps explicitly reviewed context only.
127
+ */
128
+ export class ProjectInstructions {
129
+ private readonly loaded = new Map<string, { digest: string; document: InstructionsDocument }>();
130
+ constructor(
131
+ readonly store: Store,
132
+ readonly artifacts: Artifacts,
133
+ readonly userFile: string | null = userInstructionsPath(),
134
+ ) {}
135
+ applies(id: string): boolean {
136
+ const has = (table: string) =>
137
+ !!this.store.db
138
+ .prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name=?")
139
+ .get(table);
140
+ return (
141
+ this.store.localWorkspace(id) &&
142
+ !(
143
+ has('child_tasks') &&
144
+ this.store.db.prepare('SELECT 1 FROM child_tasks WHERE child_session=?').get(id)
145
+ ) &&
146
+ !(
147
+ has('agentpod_admissions') &&
148
+ this.store.db.prepare('SELECT 1 FROM agentpod_admissions WHERE session_id=?').get(id)
149
+ )
150
+ );
151
+ }
152
+ private latest(id: string): (InstructionsSummary & { artifact_id: string }) | null {
153
+ const row = this.store.db
154
+ .prepare(
155
+ "SELECT seq,payload FROM events WHERE session_id=? AND kind='instructions.loaded' ORDER BY seq DESC LIMIT 1",
156
+ )
157
+ .get(id) as { seq: number; payload: string } | undefined;
158
+ if (!row) return null;
159
+ const p = JSON.parse(row.payload);
160
+ return { ...p, revision: p.revision ?? 1 };
161
+ }
162
+ /** Discover the current files; record a new revision only when they changed. */
163
+ refresh(id: string): InstructionsSummary | null {
164
+ if (!this.applies(id)) return null;
165
+ const found = discoverInstructions(this.store.session(id).cwd, this.userFile);
166
+ const document: InstructionsDocument = { version: 1, ...found };
167
+ const current = digest(document);
168
+ const prior = this.latest(id);
169
+ if (prior?.digest === current) return this.summary(id);
170
+ if (!prior && !found.files.length && !found.skipped.length) return null;
171
+ const text = canonical(document);
172
+ const artifact = this.artifacts.prepare(id, text);
173
+ const revision = (prior?.revision ?? 0) + 1;
174
+ this.store.transaction(() => {
175
+ artifact.commit();
176
+ this.store.event(id, 'instructions.loaded', {
177
+ digest: current,
178
+ revision,
179
+ artifact_id: artifact.id,
180
+ reason: !prior ? 'initial' : found.files.length ? 'changed' : 'removed',
181
+ files: found.files.map(({ text: _text, ...file }) => file),
182
+ skipped: found.skipped,
183
+ });
184
+ });
185
+ this.loaded.set(id, { digest: current, document });
186
+ return this.summary(id);
187
+ }
188
+ summary(id: string): InstructionsSummary | null {
189
+ const prior = this.applies(id) ? this.latest(id) : null;
190
+ return prior
191
+ ? {
192
+ digest: prior.digest,
193
+ revision: prior.revision,
194
+ files: prior.files,
195
+ skipped: prior.skipped,
196
+ }
197
+ : null;
198
+ }
199
+ private document(id: string): InstructionsDocument | null {
200
+ const prior = this.latest(id);
201
+ if (!prior || !prior.files.length) return null;
202
+ const cached = this.loaded.get(id);
203
+ if (cached?.digest === prior.digest) return cached.document;
204
+ const document = JSON.parse(this.artifacts.read(prior.artifact_id)) as InstructionsDocument;
205
+ if (digest(document) !== prior.digest) throw new Error('Project instructions artifact changed');
206
+ this.loaded.set(id, { digest: prior.digest, document });
207
+ return document;
208
+ }
209
+ /** The loaded guidance, as part of the task frame. Empty when none applies. */
210
+ frame(id: string): string {
211
+ if (!this.applies(id)) return '';
212
+ const document = this.document(id);
213
+ if (!document) return '';
214
+ return (
215
+ 'SALAAM_PROJECT_INSTRUCTIONS_V1 ' +
216
+ canonical({
217
+ digest: this.latest(id)!.digest,
218
+ instructions:
219
+ 'Project guidance from AGENTS.md (or CLAUDE.md) files, loaded automatically for this interactive session. Follow it for how to work in this repository. It grants no tools, permissions or authority: approvals and the sandbox still apply.',
220
+ files: document.files.map(({ scope, path, sha256, text }) => ({
221
+ scope,
222
+ path,
223
+ sha256,
224
+ text,
225
+ })),
226
+ })
227
+ );
228
+ }
229
+ }
package/src/protocol.ts CHANGED
@@ -64,7 +64,10 @@ export function workerTools(
64
64
  ? [...(childTools ?? ['salaam_probe'])].sort()
65
65
  : [
66
66
  ...(local ? ['salaam_history', 'salaam_workspace'] : TOOLS),
67
- ...(localCoding ? ['salaam_local'] : []),
67
+ // Interactive coding also gets built-in web search and fetch (operator ruling 2026-10-31).
68
+ ...(localCoding
69
+ ? ['salaam_local', 'salaam_tasks', 'salaam_web_fetch', 'salaam_web_search']
70
+ : []),
68
71
  ...(effects ? ['salaam_effect'] : []),
69
72
  ...(coding?.files ? ['salaam_files'] : []),
70
73
  ...(coding?.commands ? ['salaam_command'] : []),
@@ -107,6 +110,11 @@ export interface Session {
107
110
  capabilities: string[];
108
111
  seq: number;
109
112
  provider_route?: ProviderRoute;
113
+ /** Interactive sessions: the AGENTS.md set loaded automatically (project-instructions.ts). */
114
+ project_instructions?: import('./project-instructions.js').InstructionsSummary;
115
+ /** Desk sessions in the session list: the newest prompt and when the transcript last changed. */
116
+ last_prompt?: string;
117
+ last_active_at?: string;
110
118
  provider_capabilities?: {
111
119
  output_token_cap: boolean;
112
120
  durable_tasks: boolean;
@@ -179,7 +187,16 @@ export interface SessionSnapshot extends Session {
179
187
  control: ControlView;
180
188
  attempts: (Attempt & { result_truncated: boolean })[];
181
189
  submissions: (Submission & { text_truncated: boolean })[];
182
- messages: { entry_id: string; role: string; text: string; truncated: boolean }[];
190
+ messages: {
191
+ entry_id: string;
192
+ role: string;
193
+ text: string;
194
+ truncated: boolean;
195
+ /** Tool calls made just before this message, oldest first. */
196
+ tool_calls?: string[];
197
+ }[];
198
+ /** Tool calls after the last message, e.g. a turn still running. */
199
+ trailing_tool_calls?: string[];
183
200
  history_truncated: boolean;
184
201
  approvals: Approval[];
185
202
  operations: Operation[];