@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.
- package/CHANGELOG.md +41 -0
- package/dist/src/acp-desk.js +140 -0
- package/dist/src/acp-outcome.js +23 -19
- package/dist/src/acp.js +258 -11
- package/dist/src/approval-rules.js +287 -0
- package/dist/src/attach.js +3 -1
- package/dist/src/autonomy.js +80 -9
- package/dist/src/cli-help.js +34 -4
- package/dist/src/cli.js +55 -3
- package/dist/src/command-danger.js +797 -0
- package/dist/src/command-sandbox.js +116 -6
- package/dist/src/custom-commands.js +159 -0
- package/dist/src/danger-classifier.js +286 -0
- package/dist/src/git-helpers.js +160 -0
- package/dist/src/hooks.js +351 -0
- package/dist/src/host.js +96 -4
- package/dist/src/house-extension.js +133 -7
- package/dist/src/local-coding.js +553 -58
- package/dist/src/local-edits.js +168 -6
- package/dist/src/mentions.js +277 -0
- package/dist/src/pi-sessions.js +5 -1
- package/dist/src/project-instructions.js +195 -0
- package/dist/src/protocol.js +4 -1
- package/dist/src/service.js +342 -5
- package/dist/src/session-pick.js +77 -0
- package/dist/src/shell-passthrough.js +39 -0
- package/dist/src/store.js +51 -4
- package/dist/src/supervisor.js +233 -22
- package/dist/src/task-context.js +8 -2
- package/dist/src/terminal-status.js +88 -0
- package/dist/src/tui-activity.js +99 -0
- package/dist/src/tui-classifier.js +25 -0
- package/dist/src/tui-client.js +186 -5
- package/dist/src/tui-commands.js +23 -5
- package/dist/src/tui-composer.js +113 -0
- package/dist/src/tui-keymap.js +116 -0
- package/dist/src/tui-mentions.js +38 -0
- package/dist/src/tui-modals.js +157 -34
- package/dist/src/tui-notify.js +62 -0
- package/dist/src/tui-review.js +55 -0
- package/dist/src/tui-turn-keys.js +52 -0
- package/dist/src/tui-view.js +243 -69
- package/dist/src/tui.js +875 -118
- package/dist/src/turn-checkpoints.js +221 -0
- package/dist/src/turn-rewind.js +168 -0
- package/dist/src/web-fetch.js +114 -0
- package/dist/src/web-guard.js +163 -0
- package/dist/src/web-search.js +302 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/src/acp-desk.ts +155 -0
- package/src/acp-outcome.ts +25 -21
- package/src/acp.ts +338 -28
- package/src/approval-rules.ts +360 -0
- package/src/attach.ts +4 -1
- package/src/autonomy.ts +111 -10
- package/src/cli-help.ts +33 -4
- package/src/cli.ts +66 -2
- package/src/command-danger.ts +886 -0
- package/src/command-sandbox.ts +128 -8
- package/src/custom-commands.ts +175 -0
- package/src/danger-classifier.ts +321 -0
- package/src/git-helpers.ts +177 -0
- package/src/hooks.ts +425 -0
- package/src/host.ts +122 -1
- package/src/house-extension.ts +142 -7
- package/src/local-coding.ts +674 -75
- package/src/local-edits.ts +191 -8
- package/src/mentions.ts +304 -0
- package/src/pi-sessions.ts +9 -1
- package/src/project-instructions.ts +229 -0
- package/src/protocol.ts +19 -2
- package/src/service.ts +488 -5
- package/src/session-pick.ts +102 -0
- package/src/shell-passthrough.ts +38 -0
- package/src/store.ts +52 -3
- package/src/supervisor.ts +262 -24
- package/src/task-context.ts +9 -2
- package/src/terminal-status.ts +103 -0
- package/src/tui-activity.ts +133 -0
- package/src/tui-classifier.ts +38 -0
- package/src/tui-client.ts +239 -8
- package/src/tui-commands.ts +49 -10
- package/src/tui-composer.ts +124 -0
- package/src/tui-keymap.ts +152 -0
- package/src/tui-mentions.ts +43 -0
- package/src/tui-modals.ts +173 -43
- package/src/tui-notify.ts +77 -0
- package/src/tui-review.ts +76 -0
- package/src/tui-turn-keys.ts +77 -0
- package/src/tui-view.ts +341 -88
- package/src/tui.ts +1047 -137
- package/src/turn-checkpoints.ts +253 -0
- package/src/turn-rewind.ts +258 -0
- package/src/web-fetch.ts +140 -0
- package/src/web-guard.ts +195 -0
- 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
|
-
|
|
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: {
|
|
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[];
|