@anhlp/alp-core 0.0.0-beta.1
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/LICENSE +202 -0
- package/NOTICE +7 -0
- package/README.md +7 -0
- package/dist/adapter.d.ts +20 -0
- package/dist/adapter.js +30 -0
- package/dist/atomic.d.ts +2 -0
- package/dist/atomic.js +28 -0
- package/dist/backup.d.ts +27 -0
- package/dist/backup.js +582 -0
- package/dist/beads.d.ts +65 -0
- package/dist/beads.js +213 -0
- package/dist/delegation.d.ts +2 -0
- package/dist/delegation.js +32 -0
- package/dist/errors.d.ts +4 -0
- package/dist/errors.js +11 -0
- package/dist/formulas.d.ts +25 -0
- package/dist/formulas.js +201 -0
- package/dist/hook-run.d.ts +44 -0
- package/dist/hook-run.js +60 -0
- package/dist/hooks.d.ts +35 -0
- package/dist/hooks.js +30 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +63 -0
- package/dist/init.d.ts +30 -0
- package/dist/init.js +88 -0
- package/dist/library-edit.d.ts +90 -0
- package/dist/library-edit.js +315 -0
- package/dist/library.d.ts +28 -0
- package/dist/library.js +108 -0
- package/dist/log.d.ts +37 -0
- package/dist/log.js +156 -0
- package/dist/migrations.d.ts +9 -0
- package/dist/migrations.js +95 -0
- package/dist/models.d.ts +11 -0
- package/dist/models.js +44 -0
- package/dist/permissions.d.ts +14 -0
- package/dist/permissions.js +229 -0
- package/dist/profiles.d.ts +27 -0
- package/dist/profiles.js +122 -0
- package/dist/promptsafe.d.ts +1 -0
- package/dist/promptsafe.js +13 -0
- package/dist/providers.d.ts +90 -0
- package/dist/providers.js +101 -0
- package/dist/resolver.d.ts +41 -0
- package/dist/resolver.js +240 -0
- package/dist/safe-path.d.ts +2 -0
- package/dist/safe-path.js +24 -0
- package/dist/shell.d.ts +16 -0
- package/dist/shell.js +22 -0
- package/dist/state-schema.d.ts +9 -0
- package/dist/state-schema.js +54 -0
- package/dist/task-rows.d.ts +47 -0
- package/dist/task-rows.js +45 -0
- package/dist/tasks.d.ts +128 -0
- package/dist/tasks.js +835 -0
- package/dist/teams.d.ts +69 -0
- package/dist/teams.js +159 -0
- package/dist/template-path.d.ts +2 -0
- package/dist/template-path.js +10 -0
- package/dist/trust.d.ts +8 -0
- package/dist/trust.js +47 -0
- package/dist/types.d.ts +51 -0
- package/dist/upgrade.d.ts +10 -0
- package/dist/upgrade.js +200 -0
- package/dist/user-settings.d.ts +6 -0
- package/dist/user-settings.js +72 -0
- package/dist/validation.d.ts +17 -0
- package/dist/validation.js +295 -0
- package/dist/verify.d.ts +15 -0
- package/dist/verify.js +123 -0
- package/dist/workflow.d.ts +14 -0
- package/dist/workflow.js +24 -0
- package/package.json +309 -0
- package/src/adapter.d.ts +20 -0
- package/src/adapter.js +29 -0
- package/src/atomic.d.ts +2 -0
- package/src/atomic.js +28 -0
- package/src/backup.d.ts +27 -0
- package/src/backup.js +570 -0
- package/src/beads.js +236 -0
- package/src/delegation.js +28 -0
- package/src/errors.js +7 -0
- package/src/formulas.d.ts +25 -0
- package/src/formulas.js +202 -0
- package/src/hook-run.js +57 -0
- package/src/hooks.js +36 -0
- package/src/index.js +31 -0
- package/src/init.js +92 -0
- package/src/library-edit.js +346 -0
- package/src/library.js +117 -0
- package/src/log.d.ts +37 -0
- package/src/log.js +177 -0
- package/src/migrations.d.ts +9 -0
- package/src/migrations.js +111 -0
- package/src/models.js +45 -0
- package/src/permissions.d.ts +14 -0
- package/src/permissions.js +242 -0
- package/src/profiles.js +139 -0
- package/src/promptsafe.d.ts +1 -0
- package/src/promptsafe.js +19 -0
- package/src/providers.js +122 -0
- package/src/resolver.js +238 -0
- package/src/safe-path.js +20 -0
- package/src/shell.js +28 -0
- package/src/state-schema.d.ts +9 -0
- package/src/state-schema.js +60 -0
- package/src/task-rows.js +44 -0
- package/src/tasks.d.ts +128 -0
- package/src/tasks.js +881 -0
- package/src/teams.js +170 -0
- package/src/template-path.js +8 -0
- package/src/trust.js +59 -0
- package/src/types.d.ts +51 -0
- package/src/upgrade.js +195 -0
- package/src/user-settings.d.ts +6 -0
- package/src/user-settings.js +76 -0
- package/src/validation.d.ts +17 -0
- package/src/validation.js +302 -0
- package/src/verify.d.ts +15 -0
- package/src/verify.js +127 -0
- package/src/workflow.js +28 -0
- package/templates/ALP.md +52 -0
- package/templates/agents/lead/AGENT.md +90 -0
- package/templates/agents/main/AGENT.md +150 -0
- package/templates/agents/oracle/AGENT.md +44 -0
- package/templates/agents/peer/AGENT.md +55 -0
- package/templates/agents/reviewer/AGENT.md +80 -0
- package/templates/agents/supervisor/AGENT.md +72 -0
- package/templates/providers/antigravity.json +6 -0
- package/templates/providers/opencode.json +7 -0
- package/templates/role-skills.json +8 -0
- package/templates/skills/bug-loop/SKILL.md +44 -0
- package/templates/skills/bug-loop/references/test-proof.md +25 -0
- package/templates/skills/goal-griller/SKILL.md +49 -0
- package/templates/skills/prompt-leverage/SKILL.md +37 -0
- package/templates/skills/prompt-leverage/references/brief-template.md +27 -0
- package/templates/skills/sequence-execution-plan/SKILL.md +42 -0
- package/templates/skills/sequence-execution-plan/references/plan-template.md +22 -0
- package/templates/skills/smart-commits/SKILL.md +40 -0
- package/templates/skills/xia/SKILL.md +42 -0
- package/templates/teams/cafe/HOUSE_RULES.md +1 -0
- package/templates/teams/cafe/team.json +15 -0
- package/templates/teams/pho/HOUSE_RULES.md +1 -0
- package/templates/teams/pho/team.json +14 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { AlpError } from './errors.js';
|
|
3
|
+
import { MODELS } from './models.js';
|
|
4
|
+
import { validatePermissions } from './permissions.js';
|
|
5
|
+
import { validateVerify } from './verify.js';
|
|
6
|
+
|
|
7
|
+
const object = value => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
8
|
+
const nonempty = value => typeof value === 'string' && value.trim().length > 0;
|
|
9
|
+
const stringMap = value => object(value) && Object.values(value).every(v => typeof v === 'string');
|
|
10
|
+
function requireValue(condition, code, source, message) {
|
|
11
|
+
if (!condition) throw new AlpError(code, `${source}: ${message}`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Efforts a profile may name; a model ALP knows takes only its own (src/core/models.js). */
|
|
15
|
+
const EFFORTS = ['none', 'off', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra', 'ultracode'];
|
|
16
|
+
/** Top-level keys of a project's .alp/settings.json. */
|
|
17
|
+
export const PROJECT_SETTINGS = ['$schema', 'defaultAgent', 'workflow', 'runtime', 'permissions', 'verify', 'delegation', 'team', 'profile', 'templateDefaults'];
|
|
18
|
+
/** Top-level keys of the user's $ALP_HOME/settings.json. */
|
|
19
|
+
export const USER_SETTINGS = ['$schema', 'language', 'permissions', 'limits', 'recovery', 'web'];
|
|
20
|
+
/**
|
|
21
|
+
* Top-level keys ALP no longer reads, with what to use instead. They warn rather than
|
|
22
|
+
* fail, so settings written for an older ALP keep working.
|
|
23
|
+
*/
|
|
24
|
+
export const RETIRED_SETTINGS = { project: {}, user: {} };
|
|
25
|
+
|
|
26
|
+
/** The edit distance of two short keys, for suggestions. */
|
|
27
|
+
function distance(a, b) {
|
|
28
|
+
const row = Array.from({ length: b.length + 1 }, (_, index) => index);
|
|
29
|
+
for (let i = 1; i <= a.length; i++) {
|
|
30
|
+
let previous = row[0];
|
|
31
|
+
row[0] = i;
|
|
32
|
+
for (let j = 1; j <= b.length; j++) {
|
|
33
|
+
const kept = row[j];
|
|
34
|
+
row[j] = Math.min(row[j] + 1, row[j - 1] + 1, previous + (a[i - 1].toLowerCase() === b[j - 1].toLowerCase() ? 0 : 1));
|
|
35
|
+
previous = kept;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return row[b.length];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Sorts a settings object's top-level keys: unknown ones, with the closest known key
|
|
43
|
+
* when it is near, and retired ones, with what replaced them.
|
|
44
|
+
*/
|
|
45
|
+
export function settingsKeys(settings, known, retired = {}) {
|
|
46
|
+
const unknown = [];
|
|
47
|
+
const old = [];
|
|
48
|
+
for (const key of Object.keys(object(settings) ? settings : {})) {
|
|
49
|
+
if (known.includes(key)) continue;
|
|
50
|
+
if (Object.hasOwn(retired, key)) { old.push({ key, instead: retired[key] }); continue; }
|
|
51
|
+
const near = known.filter(candidate => !candidate.startsWith('$')).map(candidate => ({ candidate, d: distance(key, candidate) })).sort((a, b) => a.d - b.d)[0];
|
|
52
|
+
unknown.push({ key, ...(near && near.d <= 2 ? { suggestion: near.candidate } : {}) });
|
|
53
|
+
}
|
|
54
|
+
return { unknown, retired: old };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const unknownMessage = (unknown, known) => `unknown setting '${unknown.key}'${unknown.suggestion ? `; did you mean '${unknown.suggestion}'?` : `; known settings are ${known.filter(key => !key.startsWith('$')).join(', ')}`}`;
|
|
58
|
+
|
|
59
|
+
/** Warnings for retired keys of a settings object; unknown keys are errors instead. */
|
|
60
|
+
export function settingsWarnings(settings, scope) {
|
|
61
|
+
return settingsKeys(settings, scope === 'user' ? USER_SETTINGS : PROJECT_SETTINGS, RETIRED_SETTINGS[scope]).retired
|
|
62
|
+
.map(({ key, instead }) => `setting '${key}' is no longer read; ${instead}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function validateSettings(settings, source) {
|
|
66
|
+
const check = (ok, message) => requireValue(ok, 'INVALID_SETTINGS', source, message);
|
|
67
|
+
check(object(settings), 'expected an object');
|
|
68
|
+
const { unknown } = settingsKeys(settings, PROJECT_SETTINGS, RETIRED_SETTINGS.project);
|
|
69
|
+
check(!unknown.length, unknown.length ? unknownMessage(unknown[0], PROJECT_SETTINGS) : '');
|
|
70
|
+
if (settings.defaultAgent !== undefined) check(nonempty(settings.defaultAgent), 'defaultAgent must be a nonempty string');
|
|
71
|
+
if (settings.workflow !== undefined) {
|
|
72
|
+
check(object(settings.workflow), 'workflow must be an object');
|
|
73
|
+
check(Object.keys(settings.workflow).every(key => ['mode', 'maxPeers', 'supervisor'].includes(key)), 'unsupported workflow field');
|
|
74
|
+
// The team sessions run in (ALPD §42); a session checks that it exists. smart and supervised are Phở and Cafe's names before 0.4.
|
|
75
|
+
if (settings.workflow.mode !== undefined) check(typeof settings.workflow.mode === 'string' && /^[\w.-]+$/.test(settings.workflow.mode) && settings.workflow.mode !== 'custom', 'workflow.mode must name a team, such as pho or cafe');
|
|
76
|
+
if (settings.workflow.maxPeers !== undefined) check(Number.isSafeInteger(settings.workflow.maxPeers) && settings.workflow.maxPeers > 0, 'workflow.maxPeers must be a positive integer');
|
|
77
|
+
if (settings.workflow.supervisor !== undefined) check(typeof settings.workflow.supervisor === 'boolean', 'workflow.supervisor must be true or false');
|
|
78
|
+
}
|
|
79
|
+
if (settings.templateDefaults !== undefined) {
|
|
80
|
+
check(object(settings.templateDefaults), 'templateDefaults must map teams to profile fingerprints');
|
|
81
|
+
for (const [team, agents] of Object.entries(settings.templateDefaults)) {
|
|
82
|
+
check(/^[\w.-]+$/.test(team) && object(agents), 'templateDefaults must map teams to profile fingerprints');
|
|
83
|
+
for (const [agent, hash] of Object.entries(agents)) check(/^[\w.-]+$/.test(agent) && typeof hash === 'string' && /^[a-f0-9]{64}$/.test(hash), 'templateDefaults requires profile fingerprints');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
for (const key of ['team', 'profile']) {
|
|
87
|
+
if (settings[key] === undefined) continue;
|
|
88
|
+
check(object(settings[key]), `${key} must map team names to agent configurations`);
|
|
89
|
+
const validateChoice = (config, where) => {
|
|
90
|
+
check(object(config), `${where} must be an object`);
|
|
91
|
+
check(Object.keys(config).every(key => ['provider', 'model', 'effort'].includes(key)), `${where}: use provider, model and effort`);
|
|
92
|
+
if (config.provider !== undefined) check(['codex', 'claude', 'antigravity', 'acp'].includes(config.provider), `${where}.provider must be codex, claude or antigravity`);
|
|
93
|
+
if (config.model !== undefined) {
|
|
94
|
+
check(nonempty(config.model) && !/\s/.test(config.model), `${where}.model must be a model ID`);
|
|
95
|
+
check(config.provider !== undefined || /^(codex|claude|antigravity|acp):.+$/.test(config.model), `${where}: model needs a provider`);
|
|
96
|
+
if (config.model.includes(':')) check(/^(codex|claude|antigravity|acp):.+$/.test(config.model) && (!config.provider || config.model.startsWith(`${config.provider}:`)), `${where}: model and provider disagree`);
|
|
97
|
+
}
|
|
98
|
+
if (config.effort !== undefined) check(EFFORTS.includes(config.effort), `${where}.effort is unsupported`);
|
|
99
|
+
};
|
|
100
|
+
for (const [team, agents] of Object.entries(settings[key])) {
|
|
101
|
+
check(/^[\w.-]+$/.test(team) && object(agents), `${key}.${team} must map agent names to configurations`);
|
|
102
|
+
for (const [agent, config] of Object.entries(agents)) {
|
|
103
|
+
const where = `${key}.${team}.${agent}`;
|
|
104
|
+
check(object(config), `${where} must be an object`);
|
|
105
|
+
if (config.autocompact !== undefined) validateAutoCompact(config.autocompact, check, `${where}.autocompact`);
|
|
106
|
+
if (Object.hasOwn(config, 'profiles')) {
|
|
107
|
+
check(Object.keys(config).every(key => ['profiles', 'autocompact'].includes(key)), `${where}: use profiles and autocompact`);
|
|
108
|
+
check(Array.isArray(config.profiles) && config.profiles.length > 0, `${where}.profiles must be a nonempty list`);
|
|
109
|
+
const models = [];
|
|
110
|
+
for (const [index, entry] of config.profiles.entries()) {
|
|
111
|
+
if (typeof entry === 'string') {
|
|
112
|
+
const at = `${where}.profiles[${index}]`;
|
|
113
|
+
check(/^(codex|claude|antigravity):(\*|[^\s:*]+(:[a-z]+)?)$/.test(entry) || /^acp:[\w.-]+(?:\/[^\s:*]+)?$/.test(entry), `${at} must be "provider:model", "provider:model:effort" (such as "claude:claude-opus-5-5:high"), "acp:provider/model", or "codex:*" / "claude:*" for any model of that provider`);
|
|
114
|
+
const [provider, model, effort] = entry.split(':');
|
|
115
|
+
check(model !== '*' || (agent !== 'oracle' && agent !== 'main'), `${at}: ${agent} runs on a named model, not any model`);
|
|
116
|
+
if (effort !== undefined) {
|
|
117
|
+
const known = MODELS.find(candidate => candidate.runtime === provider && candidate.id === model);
|
|
118
|
+
check(EFFORTS.includes(effort), `${at}: effort ${effort} is unsupported`);
|
|
119
|
+
check(!known?.efforts.length || known.efforts.includes(effort), `${at}: ${provider}:${model} takes effort ${known?.efforts.join(', ')}`);
|
|
120
|
+
}
|
|
121
|
+
models.push(`${provider}:${model}`);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
validateChoice(entry, `${where}.profiles[${index}]`);
|
|
125
|
+
check(['claude', 'codex', 'antigravity'].includes(entry.provider) && nonempty(entry.model), `${where}.profiles[${index}]: specify provider and model`);
|
|
126
|
+
models.push(entry.model.includes(':') ? entry.model : `${entry.provider}:${entry.model}`);
|
|
127
|
+
}
|
|
128
|
+
check(new Set(models).size === models.length, `${where}.profiles must not repeat the same model`);
|
|
129
|
+
check(agent !== 'main' || config.profiles.length === 1, `${where}.profiles: main runs on exactly one model`);
|
|
130
|
+
} else if (agent === 'oracle') {
|
|
131
|
+
const entries = Object.entries(config).filter(([key]) => key !== 'autocompact');
|
|
132
|
+
check(entries.length > 0 && entries.every(([provider]) => ['claude', 'codex'].includes(provider)), `${where} must define profiles or claude/codex lenses`);
|
|
133
|
+
for (const [provider, lens] of entries) {
|
|
134
|
+
validateChoice(lens, `${where}.${provider}`);
|
|
135
|
+
check(lens.provider === provider && nonempty(lens.model), `${where}.${provider}: specify matching provider and model`);
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
const { autocompact, ...legacy } = config;
|
|
139
|
+
validateChoice(legacy, where);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
validatePermissions(settings.permissions, source);
|
|
145
|
+
validateVerify(settings.verify, source);
|
|
146
|
+
const runtime = settings.runtime ?? {};
|
|
147
|
+
if (settings.runtime !== undefined) check(object(settings.runtime), 'runtime must be an object');
|
|
148
|
+
for (const key of Object.keys(runtime)) {
|
|
149
|
+
check(['provider', 'model', 'reasoning'].includes(key), `unsupported runtime field '${key}'`);
|
|
150
|
+
check(nonempty(runtime[key]), `runtime.${key} must be a nonempty string`);
|
|
151
|
+
}
|
|
152
|
+
return { ...runtime };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Checks the user's $ALP_HOME/settings.json; returns it. */
|
|
156
|
+
export function validateUserSettings(settings, source) {
|
|
157
|
+
const check = (ok, message) => requireValue(ok, 'INVALID_SETTINGS', source, message);
|
|
158
|
+
check(object(settings), 'expected an object');
|
|
159
|
+
const { unknown } = settingsKeys(settings, USER_SETTINGS, RETIRED_SETTINGS.user);
|
|
160
|
+
check(!unknown.length, unknown.length ? unknownMessage(unknown[0], USER_SETTINGS) : '');
|
|
161
|
+
for (const key of ['limits', 'recovery']) {
|
|
162
|
+
if (settings[key] === undefined) continue;
|
|
163
|
+
check(object(settings[key]), `${key} must be an object`);
|
|
164
|
+
const extra = Object.keys(settings[key]).find(field => field !== 'autoResume');
|
|
165
|
+
check(!extra, `unsupported ${key} field '${extra}'; use autoResume`);
|
|
166
|
+
if (settings[key].autoResume !== undefined) check(typeof settings[key].autoResume === 'boolean', `${key}.autoResume must be true or false`);
|
|
167
|
+
}
|
|
168
|
+
if (settings.web !== undefined) {
|
|
169
|
+
check(object(settings.web), 'web must be an object');
|
|
170
|
+
const extra = Object.keys(settings.web).find(field => !['enabled', 'port', 'relay'].includes(field));
|
|
171
|
+
check(!extra, `unsupported web field '${extra}'; use enabled, port or relay`);
|
|
172
|
+
// The relay that lets Paseo's app reach alpd from another device (ALPD §64).
|
|
173
|
+
const relay = settings.web.relay;
|
|
174
|
+
if (relay !== undefined) {
|
|
175
|
+
check(object(relay), 'web.relay must be an object');
|
|
176
|
+
const unsupported = Object.keys(relay).find(field => !['enabled', 'endpoint', 'useTls'].includes(field));
|
|
177
|
+
check(!unsupported, `unsupported web.relay field '${unsupported}'; use enabled, endpoint or useTls`);
|
|
178
|
+
if (relay.enabled !== undefined) check(typeof relay.enabled === 'boolean', 'web.relay.enabled must be true or false');
|
|
179
|
+
if (relay.endpoint !== undefined) check(typeof relay.endpoint === 'string' && /^[a-z0-9.-]+:\d{1,5}$/i.test(relay.endpoint), 'web.relay.endpoint must be host:port, such as relay.paseo.sh:443');
|
|
180
|
+
if (relay.useTls !== undefined) check(typeof relay.useTls === 'boolean', 'web.relay.useTls must be true or false');
|
|
181
|
+
}
|
|
182
|
+
if (settings.web.enabled !== undefined) check(typeof settings.web.enabled === 'boolean', 'web.enabled must be true or false');
|
|
183
|
+
if (settings.web.port !== undefined) check(Number.isInteger(settings.web.port) && settings.web.port >= 1024 && settings.web.port <= 65535, 'web.port must be a port from 1024 to 65535');
|
|
184
|
+
}
|
|
185
|
+
if (settings.language !== undefined) check(nonempty(settings.language) && settings.language.trim().length <= 40, 'language must be a language name of at most 40 characters, such as Vietnamese or English');
|
|
186
|
+
validatePermissions(settings.permissions, source);
|
|
187
|
+
return settings;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Keys of an agent's agent.json (ALPD §41). */
|
|
191
|
+
export const AGENT_SETTINGS = ['$schema', 'description', 'provider', 'model', 'thinking', 'mode', 'context', 'autocompact', 'skills', 'mcp', 'hooks'];
|
|
192
|
+
/** The context window an agent may set (ALPD §57), in tokens; 'auto' is the model's. */
|
|
193
|
+
export const CONTEXT_TOKENS = { min: 100_000, max: 1_000_000 };
|
|
194
|
+
/** Use the common native compaction-window range; Codex receives the exact token limit. */
|
|
195
|
+
function validateAutoCompact(value, check, where) {
|
|
196
|
+
check(value === 'auto' || (Number.isSafeInteger(value) && value >= CONTEXT_TOKENS.min && value <= CONTEXT_TOKENS.max),
|
|
197
|
+
`${where} must be "auto" or a number of tokens from ${CONTEXT_TOKENS.min} to ${CONTEXT_TOKENS.max}`);
|
|
198
|
+
}
|
|
199
|
+
const AGENT_MODES = ['read-only', 'workspace-write', 'full-access'];
|
|
200
|
+
const entryName = value => typeof value === 'string' && /^[\w.-]+$/.test(value) && value !== '.' && value !== '..';
|
|
201
|
+
|
|
202
|
+
export const HOOK_SETTINGS = ['$schema', 'description', 'event', 'command', 'blocking', 'timeoutSec', 'match'];
|
|
203
|
+
/** The events ALP runs hooks at (ALPD §41); only those before an action can refuse it. */
|
|
204
|
+
export const HOOK_EVENTS = ['session.start', 'turn.end', 'assignment.start', 'assignment.end', 'handoff', 'task.close', 'merge'];
|
|
205
|
+
export const BLOCKING_HOOK_EVENTS = ['handoff', 'task.close', 'merge'];
|
|
206
|
+
|
|
207
|
+
/** Checks a hook definition, hooks/<name>.json: a shell command ALP runs at one of its events. */
|
|
208
|
+
export function validateHook(config, source) {
|
|
209
|
+
const check = (ok, message) => requireValue(ok, 'INVALID_HOOK', source, message);
|
|
210
|
+
check(object(config), 'expected an object');
|
|
211
|
+
const { unknown } = settingsKeys(config, HOOK_SETTINGS);
|
|
212
|
+
check(!unknown.length, unknown.length ? unknownMessage(unknown[0], HOOK_SETTINGS) : '');
|
|
213
|
+
if (config.description !== undefined) check(typeof config.description === 'string', 'description must be a string');
|
|
214
|
+
check(HOOK_EVENTS.includes(config.event), `event must be one of ${HOOK_EVENTS.join(', ')}`);
|
|
215
|
+
check(nonempty(config.command), 'command must be a nonempty string');
|
|
216
|
+
if (config.blocking !== undefined) {
|
|
217
|
+
check(typeof config.blocking === 'boolean', 'blocking must be true or false');
|
|
218
|
+
check(!config.blocking || BLOCKING_HOOK_EVENTS.includes(config.event), `only ${BLOCKING_HOOK_EVENTS.join(', ')} hooks can block`);
|
|
219
|
+
}
|
|
220
|
+
if (config.timeoutSec !== undefined) check(Number.isInteger(config.timeoutSec) && config.timeoutSec >= 1 && config.timeoutSec <= 3600, 'timeoutSec must be a whole number of seconds from 1 to 3600');
|
|
221
|
+
if (config.match !== undefined) {
|
|
222
|
+
check(object(config.match) && Object.keys(config.match).every(key => ['agent', 'label'].includes(key)), 'match may hold only agent and label');
|
|
223
|
+
for (const key of ['agent', 'label']) if (config.match[key] !== undefined) check(nonempty(config.match[key]), `match.${key} must be a nonempty string`);
|
|
224
|
+
}
|
|
225
|
+
return config;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Checks an agent's agent.json: what it runs on, its default mode, and the skills, MCP servers and hooks it names. */
|
|
229
|
+
export function validateAgentConfig(config, source) {
|
|
230
|
+
const check = (ok, message) => requireValue(ok, 'INVALID_AGENT_CONFIG', source, message);
|
|
231
|
+
check(object(config), 'expected an object');
|
|
232
|
+
const { unknown } = settingsKeys(config, AGENT_SETTINGS);
|
|
233
|
+
check(!unknown.length, unknown.length ? unknownMessage(unknown[0], AGENT_SETTINGS) : '');
|
|
234
|
+
if (config.description !== undefined) check(typeof config.description === 'string', 'description must be a string');
|
|
235
|
+
for (const key of ['provider', 'model', 'thinking']) if (config[key] !== undefined) check(nonempty(config[key]), `${key} must be a nonempty string`);
|
|
236
|
+
if (config.mode !== undefined) check(AGENT_MODES.includes(config.mode), `mode must be one of ${AGENT_MODES.join(', ')}`);
|
|
237
|
+
if (config.context !== undefined) {
|
|
238
|
+
check(config.context === 'auto' || (Number.isSafeInteger(config.context) && config.context >= CONTEXT_TOKENS.min && config.context <= CONTEXT_TOKENS.max),
|
|
239
|
+
`context must be "auto" or a number of tokens from ${CONTEXT_TOKENS.min} to ${CONTEXT_TOKENS.max}`);
|
|
240
|
+
}
|
|
241
|
+
if (config.autocompact !== undefined) validateAutoCompact(config.autocompact, check, 'autocompact');
|
|
242
|
+
for (const key of ['skills', 'mcp', 'hooks']) {
|
|
243
|
+
if (config[key] === undefined) continue;
|
|
244
|
+
check(Array.isArray(config[key]) && config[key].every(entryName), `${key} must list names of letters, digits, '.', '_' or '-'`);
|
|
245
|
+
check(new Set(config[key]).size === config[key].length, `${key} lists a name twice`);
|
|
246
|
+
}
|
|
247
|
+
return config;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function normalizeMcp(raw, directory, source) {
|
|
251
|
+
const check = (ok, message) => requireValue(ok, 'INVALID_MCP', source, message);
|
|
252
|
+
check(object(raw), 'expected an object');
|
|
253
|
+
check(Object.keys(raw).every(k => k === 'mcpServers'), 'only mcpServers is supported');
|
|
254
|
+
const servers = raw.mcpServers === undefined ? {} : raw.mcpServers;
|
|
255
|
+
check(object(servers), 'mcpServers must be an object');
|
|
256
|
+
const normalized = [];
|
|
257
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
258
|
+
check(nonempty(name) && object(server), `invalid server '${name}'`);
|
|
259
|
+
check(Object.keys(server).every(k => ['command', 'args', 'cwd', 'env', 'url', 'headers'].includes(k)), `${name}: unsupported server field`);
|
|
260
|
+
check(nonempty(server.command) !== nonempty(server.url), `${name}: specify exactly one of command or url`);
|
|
261
|
+
const result = { ...server };
|
|
262
|
+
if (server.command !== undefined) check(nonempty(server.command), `${name}.command must be nonempty`);
|
|
263
|
+
if (server.url !== undefined) {
|
|
264
|
+
check(nonempty(server.url), `${name}.url must be nonempty`);
|
|
265
|
+
let url; try { url = new URL(server.url); } catch { /* validation below */ }
|
|
266
|
+
check(url && ['https:', 'http:'].includes(url.protocol), `${name}.url must be an HTTP(S) URL`);
|
|
267
|
+
check(server.args === undefined && server.cwd === undefined && server.env === undefined, `${name}: args/cwd/env require command`);
|
|
268
|
+
}
|
|
269
|
+
if (server.args !== undefined) check(Array.isArray(server.args) && server.args.every(v => typeof v === 'string'), `${name}.args must be strings`);
|
|
270
|
+
for (const key of ['env', 'headers']) if (server[key] !== undefined) check(stringMap(server[key]), `${name}.${key} must map strings to strings`);
|
|
271
|
+
if (server.command !== undefined) check(server.headers === undefined, `${name}: headers require url`);
|
|
272
|
+
if (server.cwd !== undefined) {
|
|
273
|
+
check(nonempty(server.cwd), `${name}.cwd must be nonempty`);
|
|
274
|
+
result.cwd = path.resolve(directory, server.cwd);
|
|
275
|
+
}
|
|
276
|
+
// Bare executables remain PATH lookups; explicit relative command paths are agent-relative.
|
|
277
|
+
if (server.command && /[\\/]/.test(server.command)) result.command = path.resolve(directory, server.command);
|
|
278
|
+
normalized.push([name, result]);
|
|
279
|
+
}
|
|
280
|
+
return { mcpServers: Object.fromEntries(normalized) };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function validateResolvedAgent(agent) {
|
|
284
|
+
const check = (ok, message) => requireValue(ok, 'INVALID_RESOLVED_AGENT', 'ResolvedAgent', message);
|
|
285
|
+
check(object(agent), 'expected an object');
|
|
286
|
+
check(nonempty(agent.name), 'name is required');
|
|
287
|
+
check(typeof agent.projectRoot === 'string' && path.isAbsolute(agent.projectRoot), 'projectRoot must be absolute');
|
|
288
|
+
check(object(agent.instructions) && typeof agent.instructions.project === 'string' && typeof agent.instructions.agent === 'string', 'instructions must contain project and agent strings');
|
|
289
|
+
for (const key of ['skills', 'hooks']) {
|
|
290
|
+
check(Array.isArray(agent[key]), `${key} must be an array`);
|
|
291
|
+
for (const resource of agent[key]) check(object(resource) && nonempty(resource.name) && typeof resource.path === 'string' && path.isAbsolute(resource.path), `${key} must contain named absolute paths`);
|
|
292
|
+
}
|
|
293
|
+
check(object(agent.runtime), 'runtime must be an object');
|
|
294
|
+
validateSettings({ runtime: agent.runtime }, 'ResolvedAgent.runtime');
|
|
295
|
+
normalizeMcp(agent.mcp, agent.projectRoot, 'ResolvedAgent.mcp');
|
|
296
|
+
check(object(agent.mcp.mcpServers), 'MCP must contain a normalized mcpServers map');
|
|
297
|
+
for (const server of Object.values(agent.mcp.mcpServers)) {
|
|
298
|
+
if (server.cwd !== undefined) check(path.isAbsolute(server.cwd), 'MCP cwd must already be absolute');
|
|
299
|
+
if (server.command && /[\\/]/.test(server.command)) check(path.isAbsolute(server.command), 'MCP command paths must already be absolute');
|
|
300
|
+
}
|
|
301
|
+
return agent;
|
|
302
|
+
}
|
package/src/verify.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const VERIFY_STEPS: readonly ['setup', 'typecheck', 'test'];
|
|
2
|
+
export const VERIFY_TIMEOUT_SEC: number;
|
|
3
|
+
export const VERIFY_GRACE_MS: number;
|
|
4
|
+
export const VERIFY_INFRA_EXIT: number;
|
|
5
|
+
|
|
6
|
+
export type VerifyConfig = { setup?: string; typecheck?: string; test?: string; timeoutSec: number; idleSec?: number };
|
|
7
|
+
|
|
8
|
+
export type VerifyCommand = { step: 'setup' | 'typecheck' | 'test'; command: string; exitCode: number; ms: number; output: string; timedOut?: boolean; idle?: boolean };
|
|
9
|
+
|
|
10
|
+
export type Verification = { passed: boolean; cwd: string; commands: VerifyCommand[]; skipped?: string };
|
|
11
|
+
|
|
12
|
+
export function validateVerify(verify: unknown, source: string): VerifyConfig | undefined;
|
|
13
|
+
export function verifyConfig(projectRoot: string): Promise<VerifyConfig | undefined>;
|
|
14
|
+
export function runVerify(cwd: string, config: VerifyConfig, options?: { env?: NodeJS.ProcessEnv }): Promise<Verification>;
|
|
15
|
+
export function describeVerification(verified: { passed: boolean; commands: Array<{ step: string; exitCode: number; timedOut?: boolean; idle?: boolean }>; skipped?: string }): string;
|
package/src/verify.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { AlpError } from './errors.js';
|
|
4
|
+
import { signalShell, spawnShell } from './shell.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The project's verification gate (plans/reference/ALPD.md §28): commands from
|
|
8
|
+
* `verify` in .alp/settings.json that alpd runs before it applies an assignment's
|
|
9
|
+
* change, and after a writer in the shared checkout. They run in order and stop
|
|
10
|
+
* at the first failure.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export const VERIFY_STEPS = ['setup', 'typecheck', 'test'];
|
|
14
|
+
export const VERIFY_TIMEOUT_SEC = 600;
|
|
15
|
+
/** How long a stopped command gets between SIGTERM and SIGKILL. */
|
|
16
|
+
export const VERIFY_GRACE_MS = 5000;
|
|
17
|
+
/** The exit code (EX_TEMPFAIL) by which a step says it could not run, rather than that the change is wrong. */
|
|
18
|
+
export const VERIFY_INFRA_EXIT = 75;
|
|
19
|
+
const COMMAND_CHARS = 2000;
|
|
20
|
+
const OUTPUT_CHARS = 4000;
|
|
21
|
+
|
|
22
|
+
const object = value => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
23
|
+
const fail = (source, message) => { throw new AlpError('INVALID_SETTINGS', `${source}: ${message}`); };
|
|
24
|
+
|
|
25
|
+
/** Checks a `verify` setting; returns it normalized, or undefined when absent. */
|
|
26
|
+
export function validateVerify(verify, source) {
|
|
27
|
+
if (verify === undefined) return undefined;
|
|
28
|
+
if (!object(verify)) fail(source, 'verify must be an object');
|
|
29
|
+
const extra = Object.keys(verify).filter(key => ![...VERIFY_STEPS, 'timeoutSec', 'idleSec'].includes(key));
|
|
30
|
+
if (extra.length) fail(source, `unsupported verify field '${extra[0]}'; use ${VERIFY_STEPS.join(', ')}, timeoutSec and idleSec`);
|
|
31
|
+
for (const step of VERIFY_STEPS) {
|
|
32
|
+
if (verify[step] !== undefined && (typeof verify[step] !== 'string' || !verify[step].trim() || verify[step].length > COMMAND_CHARS)) fail(source, `verify.${step} must be a command of at most ${COMMAND_CHARS} characters`);
|
|
33
|
+
}
|
|
34
|
+
if (!VERIFY_STEPS.some(step => verify[step] !== undefined)) fail(source, `verify needs at least one of ${VERIFY_STEPS.join(', ')}`);
|
|
35
|
+
if (verify.timeoutSec !== undefined && (!Number.isSafeInteger(verify.timeoutSec) || verify.timeoutSec < 1 || verify.timeoutSec > 7200)) fail(source, 'verify.timeoutSec must be 1 to 7200 seconds');
|
|
36
|
+
if (verify.idleSec !== undefined && (!Number.isSafeInteger(verify.idleSec) || verify.idleSec < 1 || verify.idleSec > 7200)) fail(source, 'verify.idleSec must be 1 to 7200 seconds');
|
|
37
|
+
return {
|
|
38
|
+
...Object.fromEntries(VERIFY_STEPS.filter(step => verify[step] !== undefined).map(step => [step, verify[step].trim()])),
|
|
39
|
+
timeoutSec: verify.timeoutSec ?? VERIFY_TIMEOUT_SEC,
|
|
40
|
+
...(verify.idleSec !== undefined ? { idleSec: verify.idleSec } : {}),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The project's verify setting, or undefined when it has none. */
|
|
45
|
+
export async function verifyConfig(projectRoot) {
|
|
46
|
+
const file = path.join(projectRoot, '.alp', 'settings.json');
|
|
47
|
+
let text;
|
|
48
|
+
try { text = await readFile(file, 'utf8'); } catch (error) { if (error.code === 'ENOENT') return undefined; throw error; }
|
|
49
|
+
let settings;
|
|
50
|
+
try { settings = JSON.parse(text); } catch (error) { fail(file, `invalid JSON: ${error.message}`); }
|
|
51
|
+
return object(settings) ? validateVerify(settings.verify, file) : undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Runs one command through the shell, keeping the end of its output. When it runs
|
|
56
|
+
* past `timeoutMs`, or prints nothing for `idleMs`, its process group gets SIGTERM,
|
|
57
|
+
* then SIGKILL after VERIFY_GRACE_MS, so its own cleanup can run.
|
|
58
|
+
*/
|
|
59
|
+
function runCommand(command, cwd, { timeoutMs, idleMs }, env) {
|
|
60
|
+
return new Promise(resolve => {
|
|
61
|
+
const started = Date.now();
|
|
62
|
+
const child = spawnShell(command, { cwd, env, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
63
|
+
let output = '';
|
|
64
|
+
let stopped;
|
|
65
|
+
let idle;
|
|
66
|
+
const signal = name => signalShell(child, name);
|
|
67
|
+
const stop = reason => {
|
|
68
|
+
if (stopped) return;
|
|
69
|
+
stopped = reason;
|
|
70
|
+
signal('SIGTERM');
|
|
71
|
+
setTimeout(() => signal('SIGKILL'), VERIFY_GRACE_MS).unref();
|
|
72
|
+
};
|
|
73
|
+
const quiet = () => {
|
|
74
|
+
if (!idleMs) return;
|
|
75
|
+
clearTimeout(idle);
|
|
76
|
+
idle = setTimeout(() => stop('idle'), idleMs);
|
|
77
|
+
};
|
|
78
|
+
const keep = chunk => { output = (output + chunk).slice(-OUTPUT_CHARS * 2); quiet(); };
|
|
79
|
+
child.stdout.on('data', keep);
|
|
80
|
+
child.stderr.on('data', keep);
|
|
81
|
+
quiet();
|
|
82
|
+
const timer = setTimeout(() => stop('timeout'), timeoutMs);
|
|
83
|
+
let done = false;
|
|
84
|
+
const finish = (exitCode, error) => {
|
|
85
|
+
if (done) return;
|
|
86
|
+
done = true;
|
|
87
|
+
clearTimeout(timer);
|
|
88
|
+
clearTimeout(idle);
|
|
89
|
+
const tail = output.length > OUTPUT_CHARS ? `…${output.slice(-OUTPUT_CHARS)}` : output;
|
|
90
|
+
resolve({
|
|
91
|
+
exitCode, ms: Date.now() - started, output: error ? `${tail}${error}` : tail,
|
|
92
|
+
...(stopped === 'timeout' ? { timedOut: true } : stopped === 'idle' ? { idle: true } : {}),
|
|
93
|
+
...(error ? { spawnError: true } : {}),
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
child.once('error', error => finish(127, error.message));
|
|
97
|
+
child.once('close', (code, signal) => finish(stopped ? 124 : code ?? (signal ? 128 : 1)));
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Runs the configured steps in `cwd`, in order, stopping at the first failure.
|
|
103
|
+
* A step that exits VERIFY_INFRA_EXIT, or whose shell does not start, ends the run as skipped.
|
|
104
|
+
* @returns {Promise<{ passed: boolean, cwd: string, skipped?: string, commands: Array<{ step: string, command: string, exitCode: number, ms: number, output: string, timedOut?: boolean, idle?: boolean }> }>}
|
|
105
|
+
*/
|
|
106
|
+
export async function runVerify(cwd, config, { env = process.env } = {}) {
|
|
107
|
+
const commands = [];
|
|
108
|
+
for (const step of VERIFY_STEPS) {
|
|
109
|
+
if (!config[step]) continue;
|
|
110
|
+
const { spawnError, ...result } = await runCommand(config[step], cwd, { timeoutMs: config.timeoutSec * 1000, idleMs: config.idleSec ? config.idleSec * 1000 : undefined }, { ...env, ALP_VERIFY: '1' });
|
|
111
|
+
commands.push({ step, command: config[step], ...result });
|
|
112
|
+
// A step that could not run says nothing about the change: the check is skipped, not failed.
|
|
113
|
+
if (spawnError || result.exitCode === VERIFY_INFRA_EXIT) {
|
|
114
|
+
return { passed: false, cwd, commands, skipped: `infra: ${step} could not run (${spawnError ? 'the shell did not start' : `exit ${VERIFY_INFRA_EXIT}`})` };
|
|
115
|
+
}
|
|
116
|
+
if (result.exitCode !== 0) return { passed: false, cwd, commands };
|
|
117
|
+
}
|
|
118
|
+
return { passed: true, cwd, commands };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** One line for a verification: passed, or the step that failed. */
|
|
122
|
+
export function describeVerification(verified) {
|
|
123
|
+
if (verified.skipped) return `verification skipped: ${verified.skipped}`;
|
|
124
|
+
const failed = verified.commands.find(command => command.exitCode !== 0);
|
|
125
|
+
return verified.passed ? `verified (${verified.commands.map(command => command.step).join(', ')})`
|
|
126
|
+
: `verification failed: ${failed.step} ${failed.timedOut ? 'timed out' : failed.idle ? 'printed nothing for too long' : `exited ${failed.exitCode}`}`;
|
|
127
|
+
}
|
package/src/workflow.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { jsonObject } from './resolver.js';
|
|
3
|
+
import { applyProfile } from './profiles.js';
|
|
4
|
+
import { validateSettings } from './validation.js';
|
|
5
|
+
import { resolveTeam, teamOf } from './teams.js';
|
|
6
|
+
|
|
7
|
+
export { teamOf as profileOf } from './teams.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The team a session runs in (ALPD §42): the one the caller selects, the one a resumed
|
|
11
|
+
* session ran in, or settings' workflow.mode. Without any, the session is `custom`: its
|
|
12
|
+
* graph is settings' delegation and no supervisor watches it.
|
|
13
|
+
* @returns {Promise<{ mode: string, maxPeers: number, supervisor: boolean, team?: Awaited<ReturnType<typeof resolveTeam>> }>}
|
|
14
|
+
*/
|
|
15
|
+
export async function resolveWorkflow(root, selected, restored, { library, templates } = {}) {
|
|
16
|
+
const settings = await jsonObject(path.join(root, '.alp/settings.json'), 'INVALID_SETTINGS');
|
|
17
|
+
validateSettings(settings, '.alp/settings.json');
|
|
18
|
+
const chosen = selected === undefined ? undefined : teamOf(selected);
|
|
19
|
+
if (chosen === 'custom') throw new Error('Choose a team, such as pho (Phở) or cafe (Cafe)');
|
|
20
|
+
const mode = chosen ?? teamOf(restored?.mode ?? settings.workflow?.mode ?? 'custom');
|
|
21
|
+
if (restored && mode !== teamOf(restored.mode)) throw new Error('Cannot change team in an existing session');
|
|
22
|
+
const team = mode === 'custom' ? undefined : applyProfile(await resolveTeam(root, mode, { library, templates }), (settings.team ?? settings.profile)?.[mode]);
|
|
23
|
+
const maxPeers = restored?.maxPeers ?? settings.workflow?.maxPeers ?? team?.maxPeers ?? 2;
|
|
24
|
+
if (!Number.isSafeInteger(maxPeers) || maxPeers < 1) throw new Error('workflow.maxPeers must be a positive integer');
|
|
25
|
+
// A custom delegation graph has no main to supervise; settings can turn a team's supervisor off.
|
|
26
|
+
const supervisor = !!team?.supervisor && (restored?.supervisor ?? settings.workflow?.supervisor ?? true);
|
|
27
|
+
return { mode, maxPeers, supervisor, ...(team ? { team } : {}) };
|
|
28
|
+
}
|
package/templates/ALP.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# ALP project instructions
|
|
2
|
+
|
|
3
|
+
## Working relationship
|
|
4
|
+
|
|
5
|
+
The user communicates with main. The team is fixed for each session:
|
|
6
|
+
|
|
7
|
+
- Phở (default): main owns delivery and technical execution; it implements
|
|
8
|
+
directly or delegates bounded work to peer. There is no separate lead.
|
|
9
|
+
- Cafe: main owns the outcome and supervises lead; lead implements or
|
|
10
|
+
delegates to peer. Main routes implementation changes through lead.
|
|
11
|
+
|
|
12
|
+
In both, main runs by default on Opus 5.5 with high effort and full access,
|
|
13
|
+
and starts a supervisor that reviews the process after each of main's turns.
|
|
14
|
+
The supervisor asks main about mistakes; main answers and records lessons it
|
|
15
|
+
follows later.
|
|
16
|
+
|
|
17
|
+
Oracle provides read-only advice through the configured model lenses; reviewer independently
|
|
18
|
+
reviews one diff. Both return once only to the requesting coordinator. They do
|
|
19
|
+
not spawn agents. Default maximum concurrent peers is two; raise the configured
|
|
20
|
+
limit only at the user's request. Other model and effort choices belong to the
|
|
21
|
+
technical coordinator. A role grants no additional filesystem permissions,
|
|
22
|
+
credentials, or runtime tools.
|
|
23
|
+
|
|
24
|
+
## Shared work contract
|
|
25
|
+
|
|
26
|
+
- Read project instructions and the selected agent's AGENT.md. Load skills only
|
|
27
|
+
when needed; do not assume that skills or tools from another runtime exist.
|
|
28
|
+
- Each assignment states its objective, root, owned/excluded paths, constraints
|
|
29
|
+
and their sources, verification, resources, owner, and expected handoff.
|
|
30
|
+
- Keep a single writer per shared checkout. Transfer ownership before main or lead
|
|
31
|
+
intervenes in a peer's files. Parallel writers need isolated workspaces and
|
|
32
|
+
explicit shared-interface agreements.
|
|
33
|
+
- Return artifacts and real verification evidence, with remaining risks. A worker's
|
|
34
|
+
completion message is a candidate, not an independent acceptance verdict.
|
|
35
|
+
- Review should be independent of authorship: the coordinator reviews peer work;
|
|
36
|
+
in Cafe, main reviews lead work. Use reviewer for logic changes and risky
|
|
37
|
+
changes, including main-authored changes. Disclose unavailable independent review
|
|
38
|
+
rather than claiming it occurred.
|
|
39
|
+
- Changes to scope or priorities flow through the technical coordinator's current plan
|
|
40
|
+
and the affected peer's revised brief. Direct questions can receive direct answers;
|
|
41
|
+
an exceptional intervention must be reconciled with the same shared state.
|
|
42
|
+
- Tasks live in `.alp/tasks` and are shared with the user. Only the user and main
|
|
43
|
+
create or change them; other agents read them and report work they find to their
|
|
44
|
+
requester.
|
|
45
|
+
- Use only actual runtime delegation/messaging capabilities. Without them, explain
|
|
46
|
+
the limitation, perform authorized work directly where possible, and never claim
|
|
47
|
+
that a lead or peer session ran or reviewed anything.
|
|
48
|
+
|
|
49
|
+
## Project-specific constraints
|
|
50
|
+
|
|
51
|
+
Add the project's outcome, boundaries, verification commands, and authorization
|
|
52
|
+
requirements here. User instructions take precedence over this starter workflow.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Lead — technical execution owner
|
|
2
|
+
|
|
3
|
+
Receive the task from main and own technical delivery within its assigned root and
|
|
4
|
+
scope. Main represents the user's goal and coordinates the broader outcome. Route
|
|
5
|
+
questions, progress, and the final deliverable to main; contact the user directly
|
|
6
|
+
only for an explicit exception.
|
|
7
|
+
|
|
8
|
+
## Workflow and model choices
|
|
9
|
+
|
|
10
|
+
You own technical execution in Cafe. Small tasks may be completed directly;
|
|
11
|
+
peer delegation is optional. Main remains the user-facing supervisor. Phở uses
|
|
12
|
+
main as technical coordinator and does not create this role.
|
|
13
|
+
|
|
14
|
+
Choose model and effort separately for each peer assignment by the task's difficulty,
|
|
15
|
+
uncertainty, risk, and autonomy needs. When peer's model profiles offer a choice (several
|
|
16
|
+
models, or any model of a provider), pick a model they offer and its effort for every
|
|
17
|
+
assignment and say why in modelReason; otherwise use the runtime catalog. The default
|
|
18
|
+
limit is two concurrent peers. Increase only at the user's request via configuration.
|
|
19
|
+
Serialize writing assignments in the shared checkout; concurrent peers must be
|
|
20
|
+
read-only. Never bypass these limits using native spawning or shell-launched agents.
|
|
21
|
+
|
|
22
|
+
Call oracle for significant uncertainty and reviewer for logic changes or risky
|
|
23
|
+
changes; trivial typo/format edits may skip review. Brief reviewer with the
|
|
24
|
+
acceptance criteria; its verdict judges each one with evidence, and a `fail` or
|
|
25
|
+
`blocked` result goes back to work before you hand off. Oracle runs on the configured oracle models listed in your runtime instructions; for two independent
|
|
26
|
+
opinions, consult both in parallel. Never substitute another model when one is
|
|
27
|
+
unavailable; report it. Advisors return once to you; evaluate their evidence
|
|
28
|
+
and report unresolved findings to main.
|
|
29
|
+
Use available plan/usage/reset snapshots to avoid exhausted limits; missing or stale
|
|
30
|
+
data must remain explicitly uncertain and does not imply unlimited usage.
|
|
31
|
+
|
|
32
|
+
## Background first
|
|
33
|
+
|
|
34
|
+
- Delegate in the background, as alp_delegate does by default, with `etaMinutes`,
|
|
35
|
+
and go on with work that does not depend on the result. Pass `wait: true` only
|
|
36
|
+
when your next step cannot go on without it.
|
|
37
|
+
- When only results are left, alp_wait for them. Mail from main ends the wait
|
|
38
|
+
early: act on it, pass on what changes a peer's work with alp_send, then wait
|
|
39
|
+
again.
|
|
40
|
+
- Run long shell commands (builds, test suites, servers) in the background when
|
|
41
|
+
your tools allow it.
|
|
42
|
+
|
|
43
|
+
## Execution
|
|
44
|
+
|
|
45
|
+
- Inspect the current implementation and user changes before planning work.
|
|
46
|
+
- Decompose the outcome, resolve dependencies, assign ownership, and keep the plan
|
|
47
|
+
current. Make technical decisions within the brief and project constraints.
|
|
48
|
+
- Delegate bounded outcomes to peer using real runtime capabilities. A peer may
|
|
49
|
+
implement, research, design, or review according to its brief; these are tasks,
|
|
50
|
+
not additional built-in roles.
|
|
51
|
+
- Give each peer the task identity, root, objective, constraint sources, owned and
|
|
52
|
+
excluded paths, write/read authority, base revision when available, verification,
|
|
53
|
+
resources, and expected handoff. Avoid prescribing an unverified solution.
|
|
54
|
+
- Keep one writer per shared checkout. For concurrent writers allocate isolated
|
|
55
|
+
workspaces and nonconflicting resources. Main intervention requires an explicit
|
|
56
|
+
ownership transfer and an updated brief before work resumes.
|
|
57
|
+
- You may implement directly. Identify your authored changes in the handoff so
|
|
58
|
+
main can review them; do not self-accept your own implementation.
|
|
59
|
+
- Check progress and blockers through supported runtime mechanisms. Missing
|
|
60
|
+
messaging or delegation is a capability limitation, not proof of worker activity.
|
|
61
|
+
|
|
62
|
+
## Review and escalation
|
|
63
|
+
|
|
64
|
+
Review peer candidates using the actual diff/artifact and command outputs. Where
|
|
65
|
+
Git is available, verify base and candidate revisions and owned paths. Issue an
|
|
66
|
+
explicit ACCEPT or REJECT with concrete findings; test success alone is insufficient.
|
|
67
|
+
Return your aggregate outcome, changed paths, verification, peer verdicts, risks,
|
|
68
|
+
and ownership status to main. List work you or your peers found outside the scope
|
|
69
|
+
under discovered: only main and the user create tasks, and main records it. When
|
|
70
|
+
main gave you a task, your peers' claims carry its id, and your handoff moves it
|
|
71
|
+
to review for main to accept. In a non-Git project, identify the actual changed
|
|
72
|
+
files and evidence without inventing commit identifiers.
|
|
73
|
+
|
|
74
|
+
Peer objections are evidence to investigate. Resolve technical matters in scope;
|
|
75
|
+
send scope/priority conflicts or missing authority to main. When main or the user
|
|
76
|
+
changes direction, update the plan, then deliver revised briefs to affected peers
|
|
77
|
+
and confirm their receipt before relying on the new direction.
|
|
78
|
+
|
|
79
|
+
If delegation is unavailable, report that fact to main and perform authorized work
|
|
80
|
+
directly where possible. Do not claim that a peer executed or reviewed the task.
|
|
81
|
+
|
|
82
|
+
## Skills
|
|
83
|
+
|
|
84
|
+
Read the applicable SKILL.md from the runtime skill index before using its method;
|
|
85
|
+
do not preload all bodies. Use xia for unfamiliar implementation,
|
|
86
|
+
sequence-execution-plan for dependencies, and prompt-leverage before assigning peer.
|
|
87
|
+
Use bug-loop for diagnosis/corrections and smart-commits when commits are part of
|
|
88
|
+
your authorized deliverable. Identify a relevant installed method in the peer brief.
|
|
89
|
+
Intake with the user belongs to main: return material missing decisions to main,
|
|
90
|
+
including them in the handoff when there is no live upstream messaging channel.
|