@onepointfour-packs/flaredeck-agent 0.2.209 → 0.2.211
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/dist/agent.js +2 -1
- package/dist/agent.js.map +1 -1
- package/dist/assistant.d.ts +1 -1
- package/dist/assistant.js +4 -2
- package/dist/assistant.js.map +1 -1
- package/dist/check.d.ts +32 -0
- package/dist/check.js +251 -0
- package/dist/check.js.map +1 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.js.map +1 -1
- package/dist/environment.d.ts +13 -3
- package/dist/environment.js +25 -15
- package/dist/environment.js.map +1 -1
- package/dist/envtools.d.ts +29 -0
- package/dist/envtools.js +67 -5
- package/dist/envtools.js.map +1 -1
- package/dist/features/card-folder.d.ts +69 -0
- package/dist/features/card-folder.js +305 -0
- package/dist/features/card-folder.js.map +1 -0
- package/dist/features/chat.d.ts +45 -0
- package/dist/features/chat.js +90 -0
- package/dist/features/chat.js.map +1 -0
- package/dist/features/dispatch.d.ts +55 -0
- package/dist/features/dispatch.js +101 -0
- package/dist/features/dispatch.js.map +1 -0
- package/dist/features/hand-back.d.ts +60 -0
- package/dist/features/hand-back.js +127 -0
- package/dist/features/hand-back.js.map +1 -0
- package/dist/features/manage-pane.d.ts +31 -0
- package/dist/features/manage-pane.js +60 -0
- package/dist/features/manage-pane.js.map +1 -0
- package/dist/features/release-wiring.d.ts +18 -0
- package/dist/features/release-wiring.js +56 -0
- package/dist/features/release-wiring.js.map +1 -0
- package/dist/features/services.d.ts +45 -0
- package/dist/features/services.js +147 -0
- package/dist/features/services.js.map +1 -0
- package/dist/features/sign-in-request.d.ts +81 -0
- package/dist/features/sign-in-request.js +91 -0
- package/dist/features/sign-in-request.js.map +1 -0
- package/dist/github.d.ts +14 -0
- package/dist/github.js +39 -8
- package/dist/github.js.map +1 -1
- package/dist/harness/events.d.ts +58 -0
- package/dist/harness/events.js +126 -0
- package/dist/harness/events.js.map +1 -0
- package/dist/harness/handover.d.ts +39 -0
- package/dist/harness/handover.js +63 -0
- package/dist/harness/handover.js.map +1 -0
- package/dist/harness/pause.d.ts +67 -0
- package/dist/harness/pause.js +128 -0
- package/dist/harness/pause.js.map +1 -0
- package/dist/harness/plan-usage.d.ts +49 -0
- package/dist/harness/plan-usage.js +69 -0
- package/dist/harness/plan-usage.js.map +1 -0
- package/dist/harness/prompt.d.ts +59 -0
- package/dist/harness/prompt.js +289 -0
- package/dist/harness/prompt.js.map +1 -0
- package/dist/harness/repo-notes.d.ts +1 -0
- package/dist/harness/repo-notes.js +26 -0
- package/dist/harness/repo-notes.js.map +1 -0
- package/dist/harness/restarter.d.ts +33 -0
- package/dist/harness/restarter.js +78 -0
- package/dist/harness/restarter.js.map +1 -0
- package/dist/harness/start-scripts.d.ts +7 -0
- package/dist/harness/start-scripts.js +44 -0
- package/dist/harness/start-scripts.js.map +1 -0
- package/dist/harness/tools.d.ts +37 -0
- package/dist/harness/tools.js +17 -0
- package/dist/harness/tools.js.map +1 -0
- package/dist/hydrate.d.ts +5 -0
- package/dist/hydrate.js +1 -0
- package/dist/hydrate.js.map +1 -1
- package/dist/main.js +32 -1
- package/dist/main.js.map +1 -1
- package/dist/profile.d.ts +80 -0
- package/dist/profile.js +77 -0
- package/dist/profile.js.map +1 -0
- package/dist/release.js +3 -0
- package/dist/release.js.map +1 -1
- package/dist/sandbox.js +3 -1
- package/dist/sandbox.js.map +1 -1
- package/dist/serve.d.ts +21 -115
- package/dist/serve.js +971 -1612
- package/dist/serve.js.map +1 -1
- package/dist/stream.d.ts +5 -0
- package/dist/stream.js.map +1 -1
- package/dist/workspace.d.ts +6 -0
- package/dist/workspace.js +19 -0
- package/dist/workspace.js.map +1 -1
- package/dist/worktree.d.ts +55 -2
- package/dist/worktree.js +130 -5
- package/dist/worktree.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A card's folder: every repository the card changes or needs, each a worktree on the card's
|
|
3
|
+
* branch, side by side.
|
|
4
|
+
*
|
|
5
|
+
* <checkouts>/.worktrees-cards/agent-car-0086/
|
|
6
|
+
* BankServAfrica.Careers/ ← the card's main repository (the turn starts here)
|
|
7
|
+
* BankServAfrica.Intranet/ ← what it needs, on the same branch name
|
|
8
|
+
* BankservAfricaCMSIntranetAPI/
|
|
9
|
+
*
|
|
10
|
+
* The harness cannot know whether a Careers card will also need an API change — the agent finds
|
|
11
|
+
* that out while working. So it is given all of them from the start, may change any, and the
|
|
12
|
+
* environment runs every one from its worktree: the site always talks to the API and CMS code in
|
|
13
|
+
* the same folder, so a change in any of them is what is tested. Only repositories with commits
|
|
14
|
+
* are published. A worktree shares its repository's history, so the extra checkouts cost seconds
|
|
15
|
+
* and their working files; node_modules is cloned copy-on-write (cloneDependencies).
|
|
16
|
+
*
|
|
17
|
+
* A card with one repository has no folder and works exactly as before.
|
|
18
|
+
*/
|
|
19
|
+
import { existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
20
|
+
import { basename, dirname, join } from 'node:path';
|
|
21
|
+
import { CARD_FOLDERS, branchFor, ensureWorktree, releaseWorktree, repoOfWorktree, runGit } from '../worktree.js';
|
|
22
|
+
import { repoIdentity } from '../workspace.js';
|
|
23
|
+
import { deployedFor, envName, serviceDir } from './services.js';
|
|
24
|
+
/** More than this many repositories is a board's whole estate, not one card's work: no folder. */
|
|
25
|
+
export const MAX_FOLDER_REPOS = 5;
|
|
26
|
+
/** The folder for a card, beside the checkout its main repository was cut from. */
|
|
27
|
+
export const cardFolderFor = (mainRepo, card) => join(dirname(mainRepo.replace(/\/+$/, '')), CARD_FOLDERS, branchFor(card).replace(/\//g, '-'));
|
|
28
|
+
/** Whether a worktree path is inside a card folder, and which. */
|
|
29
|
+
export function folderOf(path) {
|
|
30
|
+
const m = new RegExp(`^(.*/${CARD_FOLDERS.replace(/\./g, '\\.')}/[^/]+)/[^/]+/?$`).exec(path);
|
|
31
|
+
return m ? m[1] : null;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The repositories beside the main one: everything the card changes, and everything it runs
|
|
35
|
+
* alongside, that is not the main repository. A card whose board lists more than MAX_FOLDER_REPOS
|
|
36
|
+
* gets only what it runs alongside — a legacy board with no workspace list sends every repository
|
|
37
|
+
* it has, which is the board's estate rather than this card's.
|
|
38
|
+
*/
|
|
39
|
+
export function companionRepos(event, mainOrigin) {
|
|
40
|
+
const main = repoIdentity(mainOrigin);
|
|
41
|
+
const seen = new Set([main]);
|
|
42
|
+
const pick = (list) => list.filter((r) => {
|
|
43
|
+
const id = repoIdentity(r.repository ?? '');
|
|
44
|
+
if (!r.repository || seen.has(id))
|
|
45
|
+
return false;
|
|
46
|
+
seen.add(id);
|
|
47
|
+
return true;
|
|
48
|
+
});
|
|
49
|
+
const changes = (event.baseBranches ?? []).filter((r) => r.repository);
|
|
50
|
+
const alongside = event.alongside ?? [];
|
|
51
|
+
const everything = new Set([...changes, ...alongside].map((r) => repoIdentity(r.repository)));
|
|
52
|
+
return everything.size > MAX_FOLDER_REPOS ? pick(alongside) : [...pick(changes), ...pick(alongside)];
|
|
53
|
+
}
|
|
54
|
+
/** A folder name for a repository, unique within the card's folder. */
|
|
55
|
+
const nameFor = (repository, taken) => {
|
|
56
|
+
const base = repository.replace(/\.git$/i, '').replace(/\/+$/, '').split('/').pop() || serviceDir(repository);
|
|
57
|
+
let name = base;
|
|
58
|
+
for (let i = 2; taken.has(name.toLowerCase()); i++)
|
|
59
|
+
name = `${base}-${i}`;
|
|
60
|
+
taken.add(name.toLowerCase());
|
|
61
|
+
return name;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Make (or reuse) the card's folder: the main repository's worktree and one per companion. Null
|
|
65
|
+
* when the main one cannot be made — the caller then falls back to a lone worktree. A companion
|
|
66
|
+
* that cannot be made is said and left out; the card still has its main repository.
|
|
67
|
+
*/
|
|
68
|
+
export async function openCardFolder(mainRepo, card, base, companions, d) {
|
|
69
|
+
const folder = cardFolderFor(mainRepo, card);
|
|
70
|
+
mkdirSync(folder, { recursive: true });
|
|
71
|
+
const taken = new Set();
|
|
72
|
+
const mainName = nameFor(basename(mainRepo), taken);
|
|
73
|
+
const main = await ensureWorktree(mainRepo, card, d.onStep, base, d.local, { path: join(folder, mainName), meta: { main: 'true', ...(base ? { base } : {}) } });
|
|
74
|
+
if (!main)
|
|
75
|
+
return null;
|
|
76
|
+
const made = [];
|
|
77
|
+
for (const r of companions) {
|
|
78
|
+
const name = nameFor(r.repository, taken);
|
|
79
|
+
const src = await d.source(r.repository);
|
|
80
|
+
if (!src) {
|
|
81
|
+
d.onStep(`${name}: not on this machine and could not be cloned — left out of the card's folder`);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const t = await ensureWorktree(src, card, (s) => d.onStep(`${name}: ${s}`), r.branch || undefined, d.local, {
|
|
85
|
+
path: join(folder, name),
|
|
86
|
+
meta: { repository: r.repository, type: r.component || '', ...(r.branch ? { base: r.branch } : {}), ...(r.environmentScript ? { script: r.environmentScript } : {}), ...(r.sites && Object.keys(r.sites).length ? { sites: JSON.stringify(r.sites) } : {}) },
|
|
87
|
+
});
|
|
88
|
+
if (!t) {
|
|
89
|
+
d.onStep(`${name}: could not make its worktree — left out of the card's folder`);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
made.push({
|
|
93
|
+
...t, name, repository: r.repository, type: r.component || '', base: r.branch || (await defaultBranchOf(t.path)),
|
|
94
|
+
...(r.environmentScript ? { script: r.environmentScript } : {}), ...(r.sites && Object.keys(r.sites).length ? { sites: r.sites } : {}),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return { ...main, folder, companions: made };
|
|
98
|
+
}
|
|
99
|
+
async function defaultBranchOf(path) {
|
|
100
|
+
const head = await runGit(['symbolic-ref', '--quiet', '--short', 'refs/remotes/origin/HEAD'], path);
|
|
101
|
+
return head.code === 0 && head.out ? head.out.trim().replace(/^origin\//, '') : 'main';
|
|
102
|
+
}
|
|
103
|
+
const meta = async (path, key) => (await runGit(['config', '--worktree', '--get', `flaredeck.${key}`], path)).out.trim();
|
|
104
|
+
/**
|
|
105
|
+
* A card folder found again — after a restart, or by a later turn that did not make it: the main
|
|
106
|
+
* worktree's companions, read back from each worktree's own config.
|
|
107
|
+
*/
|
|
108
|
+
export async function withCompanions(tree) {
|
|
109
|
+
const folder = folderOf(tree.path);
|
|
110
|
+
if (!folder || tree.companions)
|
|
111
|
+
return tree;
|
|
112
|
+
let names = [];
|
|
113
|
+
try {
|
|
114
|
+
names = readdirSync(folder);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return tree;
|
|
118
|
+
}
|
|
119
|
+
const companions = [];
|
|
120
|
+
for (const name of names.sort()) {
|
|
121
|
+
const path = join(folder, name);
|
|
122
|
+
if (path === tree.path || !existsSync(join(path, '.git')))
|
|
123
|
+
continue;
|
|
124
|
+
const repository = (await meta(path, 'repository')) || (await runGit(['remote', 'get-url', 'origin'], path)).out.trim();
|
|
125
|
+
const branch = (await runGit(['rev-parse', '--abbrev-ref', 'HEAD'], path)).out.trim();
|
|
126
|
+
const at = await meta(path, 'at');
|
|
127
|
+
const sites = await meta(path, 'sites');
|
|
128
|
+
const script = await meta(path, 'script');
|
|
129
|
+
let parsed;
|
|
130
|
+
try {
|
|
131
|
+
parsed = sites ? JSON.parse(sites) : undefined;
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
parsed = undefined;
|
|
135
|
+
}
|
|
136
|
+
companions.push({
|
|
137
|
+
path, branch, created: false, ...(at ? { at } : {}), name, repository,
|
|
138
|
+
type: await meta(path, 'type'), base: (await meta(path, 'base')) || (await defaultBranchOf(path)),
|
|
139
|
+
...(script ? { script } : {}), ...(parsed ? { sites: parsed } : {}),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
return { ...tree, folder, companions };
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Release every worktree in the card's folder that holds nothing unpublished, and the folder once
|
|
146
|
+
* it is empty. What cannot be released is returned, one reason each.
|
|
147
|
+
*/
|
|
148
|
+
export async function releaseCardFolder(tree, card, onStep) {
|
|
149
|
+
const kept = [];
|
|
150
|
+
for (const t of [tree, ...(tree.companions ?? [])]) {
|
|
151
|
+
const repo = await repoOfWorktree(t.path);
|
|
152
|
+
if (!repo)
|
|
153
|
+
continue;
|
|
154
|
+
const r = await releaseWorktree(repo, card, onStep, t.path);
|
|
155
|
+
if (!r.removed && r.why !== 'there was no worktree')
|
|
156
|
+
kept.push(`${basename(t.path)}: ${r.why}`);
|
|
157
|
+
}
|
|
158
|
+
const folder = tree.folder ?? folderOf(tree.path);
|
|
159
|
+
if (folder && !kept.length) {
|
|
160
|
+
try {
|
|
161
|
+
rmSync(join(folder, 'start.sh'), { force: true });
|
|
162
|
+
if (!readdirSync(folder).length)
|
|
163
|
+
rmSync(folder, { recursive: true, force: true });
|
|
164
|
+
}
|
|
165
|
+
catch { /* gone */ }
|
|
166
|
+
}
|
|
167
|
+
return kept.length ? { removed: false, why: kept.join('; ') } : { removed: true, why: '' };
|
|
168
|
+
}
|
|
169
|
+
/** What the agent is told about its folder. */
|
|
170
|
+
export function folderBrief(tree) {
|
|
171
|
+
if (!tree.folder || !tree.companions?.length)
|
|
172
|
+
return '';
|
|
173
|
+
const line = (name, type, path, base) => ` · ${name}${type ? ` (${type})` : ''} — ${path}, cut from ${base}`;
|
|
174
|
+
return (`\n\n## Your card's folder\n\n` +
|
|
175
|
+
`This card has a folder of its own, ${tree.folder}, holding every repository it touches — each a ` +
|
|
176
|
+
`checkout on ${tree.branch}:\n` +
|
|
177
|
+
`${line(basename(tree.path), 'the card\'s own', tree.path, tree.from?.replace(/^origin\//, '') || 'its base')}\n` +
|
|
178
|
+
`${tree.companions.map((c) => line(c.name, c.type, c.path, c.base)).join('\n')}\n\n` +
|
|
179
|
+
`You may change any of them: when the fix belongs in the API or the CMS, make it there. Commit in the ` +
|
|
180
|
+
`repository you changed, and publish each one you changed with push_branch (or submit_work) giving ` +
|
|
181
|
+
`\`repository\` its folder name; the card's own needs none. A repository you did not change is not ` +
|
|
182
|
+
`published. The environment runs every one of them from this folder, so the app you test is using the ` +
|
|
183
|
+
`code here — after changing one, restart it with start_environment's \`repositories\`. (The folder's start.sh is ` +
|
|
184
|
+
`for people; use start_environment, which keeps track of what it started.)`);
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Start each companion from its worktree, in parallel (none of them needs another — what they
|
|
188
|
+
* need is the main repository's business), and give the main one their addresses. A companion
|
|
189
|
+
* that cannot be started here uses its deployed address for the environment, if it has one.
|
|
190
|
+
*/
|
|
191
|
+
export async function startCompanions(companions, environment, d) {
|
|
192
|
+
const env = {};
|
|
193
|
+
const notes = [];
|
|
194
|
+
const started = new Map();
|
|
195
|
+
const results = await Promise.all(companions.map(async (c) => {
|
|
196
|
+
const stored = d.storedScript(c.repository);
|
|
197
|
+
const s = await d.start(c.path, (t) => d.log(`${c.name}: ${t}`), c.script, stored ? { script: stored } : {}).catch((e) => ({ ran: true, ok: false, endpoints: {}, detail: String(e) }));
|
|
198
|
+
return { c, s };
|
|
199
|
+
}));
|
|
200
|
+
for (const { c, s } of results) {
|
|
201
|
+
const give = (url) => { env[envName(c.type || 'service')] ??= url; env[envName(serviceDir(c.repository))] = url; };
|
|
202
|
+
const deployed = deployedFor(c.sites, environment);
|
|
203
|
+
if (s.ran && s.ok) {
|
|
204
|
+
started.set(c.path, s);
|
|
205
|
+
const url = Object.values(s.endpoints)[0];
|
|
206
|
+
if (url) {
|
|
207
|
+
give(url);
|
|
208
|
+
notes.push(`${c.name}${c.type ? ` (${c.type})` : ''}: running from your folder, ${url}`);
|
|
209
|
+
}
|
|
210
|
+
else
|
|
211
|
+
notes.push(`${c.name}: started from your folder, but printed no address`);
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
const why = !s.ran ? 'it has no start script' : `it did not start — ${s.detail.split('\n').filter(Boolean).pop() ?? ''}`;
|
|
215
|
+
// A missing script is the agent's to write, once, for every card after this one.
|
|
216
|
+
const fix = !s.ran
|
|
217
|
+
? ` Write one to the start-script contract (save_start_script describes it) and keep it with save_start_script, repository "${c.name}": every card after this one then runs it from its folder.`
|
|
218
|
+
: ` If the script is wrong, fix it and keep the fixed one with save_start_script, repository "${c.name}".`;
|
|
219
|
+
if (deployed) {
|
|
220
|
+
give(deployed);
|
|
221
|
+
notes.push(`${c.name}: ${why}, so the deployed one is used, ${deployed} — your changes to it are NOT what the app is talking to.${fix}`);
|
|
222
|
+
}
|
|
223
|
+
else
|
|
224
|
+
notes.push(`${c.name}: not running — ${why}.${fix}`);
|
|
225
|
+
}
|
|
226
|
+
return { env, notes, started };
|
|
227
|
+
}
|
|
228
|
+
/** A shell word, quoted. */
|
|
229
|
+
const q = (v) => `'${v.replace(/'/g, `'\\''`)}'`;
|
|
230
|
+
/**
|
|
231
|
+
* `start.sh` at the root of the card's folder, for a PERSON: `./start.sh` brings up every
|
|
232
|
+
* repository in it — the others first, then the card's own with their addresses — `./start.sh A B`
|
|
233
|
+
* just those, `./start.sh down [A…]` stops them. Each repository's own script does the work (its
|
|
234
|
+
* committed one, else the one kept in FlareDeck), exactly as the harness runs it. Agents use
|
|
235
|
+
* start_environment, which records what it started; the sandbox sends them there.
|
|
236
|
+
*/
|
|
237
|
+
export function folderScript(tree, stored, mainRepository, mainScript) {
|
|
238
|
+
const cs = tree.companions ?? [];
|
|
239
|
+
const main = { name: basename(tree.path), type: '', repository: mainRepository, script: mainScript, sites: undefined };
|
|
240
|
+
const all = [...cs.map((c) => ({ name: c.name, type: c.type, repository: c.repository, script: c.script, sites: c.sites })), main];
|
|
241
|
+
const cases = (f) => all.map((r) => ` ${q(r.name)}) echo ${q(f(r))};;`).join('\n');
|
|
242
|
+
return `#!/usr/bin/env bash
|
|
243
|
+
# This card's folder: ${tree.folder}
|
|
244
|
+
# Written by flaredeck-agent for people. Agents start it with start_environment.
|
|
245
|
+
#
|
|
246
|
+
# ./start.sh start everything — ${cs.map((c) => c.name).join(', ')}, then ${main.name} with their addresses
|
|
247
|
+
# ./start.sh NAME… start just those
|
|
248
|
+
# ./start.sh down [NAME…] stop them
|
|
249
|
+
set -uo pipefail
|
|
250
|
+
cd "$(dirname "$0")"
|
|
251
|
+
ORDER=(${all.map((r) => q(r.name)).join(' ')})
|
|
252
|
+
declared() { case "$1" in
|
|
253
|
+
${cases((r) => r.script || '.flaredeck/start.sh')}
|
|
254
|
+
esac; }
|
|
255
|
+
kept() { case "$1" in
|
|
256
|
+
${cases((r) => stored(r.repository) ?? '')}
|
|
257
|
+
esac; }
|
|
258
|
+
vars() { case "$1" in
|
|
259
|
+
${cases((r) => r.name === main.name ? '' : `${envName(r.type || 'service')} ${envName(serviceDir(r.repository))}`)}
|
|
260
|
+
esac; }
|
|
261
|
+
deployed() { case "$1" in
|
|
262
|
+
${cases((r) => (r.sites ? Object.values(r.sites)[0] : '') ?? '')}
|
|
263
|
+
esac; }
|
|
264
|
+
script_of() { local d; d="$(declared "$1")"; if [ -f "$1/$d" ]; then echo "$d"; else kept "$1"; fi; }
|
|
265
|
+
DOWN=""; [ "\${1:-}" = down ] && { DOWN=down; shift; }
|
|
266
|
+
WANT=("$@"); [ \${#WANT[@]} -eq 0 ] && WANT=("\${ORDER[@]}")
|
|
267
|
+
wanted() { local w; for w in "\${WANT[@]}"; do [ "$w" = "$1" ] && return 0; done; return 1; }
|
|
268
|
+
rc=0
|
|
269
|
+
for name in "\${ORDER[@]}"; do
|
|
270
|
+
wanted "$name" || continue
|
|
271
|
+
s="$(script_of "$name")"
|
|
272
|
+
if [ -z "$s" ]; then
|
|
273
|
+
d="$(deployed "$name")"
|
|
274
|
+
if [ -n "$d" ] && [ -z "$DOWN" ]; then for v in $(vars "$name"); do export "$v=$d"; done; echo "$name: no start script — using the deployed $d"; fi
|
|
275
|
+
[ -z "$d" ] && echo "$name: no start script" >&2
|
|
276
|
+
continue
|
|
277
|
+
fi
|
|
278
|
+
out="$(cd "$name" && bash "$s" $DOWN)"; code=$?
|
|
279
|
+
if [ -n "$DOWN" ]; then
|
|
280
|
+
# What still runs from this repository's folder goes too, as the harness does: a script whose
|
|
281
|
+
# \`down\` does nothing (or starts another copy) must not leave servers behind.
|
|
282
|
+
pkill -f "$PWD/$name/" 2>/dev/null && echo "$name: stopped what was still running from it"
|
|
283
|
+
[ $code -ne 0 ] && echo "$name: its down exited $code" >&2
|
|
284
|
+
continue
|
|
285
|
+
fi
|
|
286
|
+
[ -n "$out" ] && printf '%s\\n' "$out" | sed "s/^/$name: /"
|
|
287
|
+
[ $code -ne 0 ] && { echo "$name: exited $code" >&2; rc=$code; continue; }
|
|
288
|
+
if true; then
|
|
289
|
+
url="$(printf '%s\\n' "$out" | grep -E '^[A-Za-z0-9_.-]+=https?://' | head -1 | cut -d= -f2-)"
|
|
290
|
+
[ -n "$url" ] && for v in $(vars "$name"); do export "$v=$url"; done
|
|
291
|
+
fi
|
|
292
|
+
done
|
|
293
|
+
exit $rc
|
|
294
|
+
`;
|
|
295
|
+
}
|
|
296
|
+
/** Write the folder's start.sh (see folderScript). Never fails the card. */
|
|
297
|
+
export function writeFolderScript(tree, stored, mainRepository, mainScript) {
|
|
298
|
+
if (!tree.folder || !tree.companions?.length)
|
|
299
|
+
return;
|
|
300
|
+
try {
|
|
301
|
+
writeFileSync(join(tree.folder, 'start.sh'), folderScript(tree, stored, mainRepository, mainScript), { mode: 0o755 });
|
|
302
|
+
}
|
|
303
|
+
catch { /* a convenience */ }
|
|
304
|
+
}
|
|
305
|
+
//# sourceMappingURL=card-folder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"card-folder.js","sourceRoot":"","sources":["../../src/features/card-folder.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { StreamEvent } from '../stream.js';
|
|
2
|
+
import type { AgentEvent } from '../agent.js';
|
|
3
|
+
import { ASSISTANT_KEY, type AssistantPerson } from '../assistant.js';
|
|
4
|
+
export { ASSISTANT_KEY };
|
|
5
|
+
export type ChatDeps = {
|
|
6
|
+
/** The agent kind, for what it calls itself. */
|
|
7
|
+
kind: () => 'assistant' | 'general';
|
|
8
|
+
mcpUrl: string;
|
|
9
|
+
auth: () => Promise<string>;
|
|
10
|
+
/** The folder the harness was started in: where the person's own Claude sessions live. */
|
|
11
|
+
repo: string;
|
|
12
|
+
/** Where the conversation's session id is kept between runs. */
|
|
13
|
+
sessionFile: string;
|
|
14
|
+
/** This agent's name, for the history message. */
|
|
15
|
+
agent: string;
|
|
16
|
+
/** Send a FlareDeck message to the person. */
|
|
17
|
+
send: (to: string, message: string) => Promise<void>;
|
|
18
|
+
log: (kind: 'event' | 'tool' | 'warn', text: string) => void;
|
|
19
|
+
};
|
|
20
|
+
export declare function createChat(d: ChatDeps): {
|
|
21
|
+
person: () => AssistantPerson | null;
|
|
22
|
+
/** Who it works for, from FlareDeck. Said once at start, so a missing "Works for" is plain. */
|
|
23
|
+
start(tenant: string | null): Promise<void>;
|
|
24
|
+
/** Why this event is not for it (someone else talking), or null when it is its person's. */
|
|
25
|
+
ignores: (event: StreamEvent) => string | null;
|
|
26
|
+
/** An event in the conversation, worded as its person's words. */
|
|
27
|
+
asTurn: (event: StreamEvent) => StreamEvent;
|
|
28
|
+
/** Their message while a turn runs: what is steered into it. */
|
|
29
|
+
midTurn: (event: StreamEvent) => string;
|
|
30
|
+
/** The conversation from before a restart, if one was kept. */
|
|
31
|
+
kept: () => string | null;
|
|
32
|
+
keep: (sessionId: string) => void;
|
|
33
|
+
/**
|
|
34
|
+
* Its very first turn, with no conversation of its own: its person's newest Claude CLI session
|
|
35
|
+
* in this folder, carried on as a FORK — the history comes across, their own session file is
|
|
36
|
+
* left as it was. The last thirty exchanges are posted to their chat once, marked as the past,
|
|
37
|
+
* so they see in FlareDeck where the conversation was. Returns the session to fork, or null.
|
|
38
|
+
*/
|
|
39
|
+
pickUp(): Promise<string | null>;
|
|
40
|
+
/** The turn, as its person watches it in FlareDeck chat: the main conversation, not sub-agents. */
|
|
41
|
+
turnStarted: (event: StreamEvent) => void | undefined;
|
|
42
|
+
observe(out: AgentEvent): void;
|
|
43
|
+
turnEnded: () => undefined;
|
|
44
|
+
};
|
|
45
|
+
export type Chat = ReturnType<typeof createChat>;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `chat` feature: one long conversation with the one person an agent works for — an assistant,
|
|
3
|
+
* or a general agent. Everything that makes it different from card work lives here:
|
|
4
|
+
*
|
|
5
|
+
* · who that person is ("Works for" in FlareDeck), and that only their words reach it;
|
|
6
|
+
* · their message while a turn runs goes INTO that turn, not to the back of a queue;
|
|
7
|
+
* · the running turn shown live in their FlareDeck chat;
|
|
8
|
+
* · the conversation kept across restarts, and — on the very first turn — carried on from their
|
|
9
|
+
* own Claude CLI session in this folder, with the last exchanges posted to the chat once.
|
|
10
|
+
*
|
|
11
|
+
* Cards a person gives it are not chat: those are worked like any card (serve.ts, assistantCard).
|
|
12
|
+
* Split out of serve.ts (docs/spec-harness-profiles.md, step 2).
|
|
13
|
+
*/
|
|
14
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
15
|
+
import { dirname, join } from 'node:path';
|
|
16
|
+
import { ASSISTANT_KEY, LiveFeed, asAssistantEvent, assistantIgnores, assistantPerson, claudeProjectDir, historyMessage, latestCliSession, liveSender, loadAssistantSession, midTurnMessage, recentExchanges, saveAssistantSession, } from '../assistant.js';
|
|
17
|
+
export { ASSISTANT_KEY };
|
|
18
|
+
export function createChat(d) {
|
|
19
|
+
let person = null;
|
|
20
|
+
let live = null;
|
|
21
|
+
const name = () => person?.name ?? 'your person';
|
|
22
|
+
return {
|
|
23
|
+
person: () => person,
|
|
24
|
+
/** Who it works for, from FlareDeck. Said once at start, so a missing "Works for" is plain. */
|
|
25
|
+
async start(tenant) {
|
|
26
|
+
person = tenant ? await assistantPerson(d.mcpUrl, tenant, d.auth) : null;
|
|
27
|
+
live = tenant && person ? new LiveFeed(liveSender(d.mcpUrl, tenant, d.auth)) : null;
|
|
28
|
+
const kept = loadAssistantSession(d.sessionFile);
|
|
29
|
+
const what = d.kind() === 'general' ? 'coding agent' : 'assistant';
|
|
30
|
+
d.log('event', person
|
|
31
|
+
? `You are ${person.name}'s ${what} — you listen to them in FlareDeck chat, and to whoever types here${kept ? ', carrying on your conversation from before' : ''}.`
|
|
32
|
+
: `You are ${what === 'assistant' ? 'an' : 'a'} ${what}, but nobody is set as the person you work for — set "Works for" on this agent in FlareDeck. Until then nothing reaches you.`);
|
|
33
|
+
},
|
|
34
|
+
/** Why this event is not for it (someone else talking), or null when it is its person's. */
|
|
35
|
+
ignores: (event) => assistantIgnores(event, person?.uid ?? null),
|
|
36
|
+
/** An event in the conversation, worded as its person's words. */
|
|
37
|
+
asTurn: (event) => asAssistantEvent(event, name()),
|
|
38
|
+
/** Their message while a turn runs: what is steered into it. */
|
|
39
|
+
midTurn: (event) => midTurnMessage(event, name()),
|
|
40
|
+
/** The conversation from before a restart, if one was kept. */
|
|
41
|
+
kept: () => loadAssistantSession(d.sessionFile),
|
|
42
|
+
keep: (sessionId) => saveAssistantSession(d.sessionFile, sessionId),
|
|
43
|
+
/**
|
|
44
|
+
* Its very first turn, with no conversation of its own: its person's newest Claude CLI session
|
|
45
|
+
* in this folder, carried on as a FORK — the history comes across, their own session file is
|
|
46
|
+
* left as it was. The last thirty exchanges are posted to their chat once, marked as the past,
|
|
47
|
+
* so they see in FlareDeck where the conversation was. Returns the session to fork, or null.
|
|
48
|
+
*/
|
|
49
|
+
async pickUp() {
|
|
50
|
+
const picked = latestCliSession(d.repo);
|
|
51
|
+
if (!picked)
|
|
52
|
+
return null;
|
|
53
|
+
d.log('event', `carrying on from your Claude session ${picked.slice(0, 8)} in this folder (a copy — yours is left as it is)`);
|
|
54
|
+
const posted = `${d.sessionFile}.history`;
|
|
55
|
+
if (person && !existsSync(posted)) {
|
|
56
|
+
const ex = recentExchanges(join(claudeProjectDir(d.repo), `${picked}.jsonl`));
|
|
57
|
+
if (ex.length) {
|
|
58
|
+
const p = person;
|
|
59
|
+
await d.send(p.uid, historyMessage(ex, d.agent))
|
|
60
|
+
.then(() => {
|
|
61
|
+
try {
|
|
62
|
+
mkdirSync(dirname(posted), { recursive: true });
|
|
63
|
+
writeFileSync(posted, picked);
|
|
64
|
+
}
|
|
65
|
+
catch { /* posted; a repeat is harmless */ }
|
|
66
|
+
d.log('tool', `posted the last ${ex.length} exchanges to ${p.name}'s chat`);
|
|
67
|
+
})
|
|
68
|
+
.catch((e) => d.log('warn', `could not post the earlier conversation to the chat: ${e instanceof Error ? e.message : String(e)}`));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return picked;
|
|
72
|
+
},
|
|
73
|
+
/** The turn, as its person watches it in FlareDeck chat: the main conversation, not sub-agents. */
|
|
74
|
+
turnStarted: (event) => live?.start(String(event.message ?? event.line ?? '').split('\n').filter(Boolean).pop() ?? ''),
|
|
75
|
+
observe(out) {
|
|
76
|
+
if (!live)
|
|
77
|
+
return;
|
|
78
|
+
if (out.kind === 'text' && !out.sub)
|
|
79
|
+
live.add('text', out.text);
|
|
80
|
+
else if (out.kind === 'tool' && !out.sub)
|
|
81
|
+
live.add('tool', out.name);
|
|
82
|
+
else if (out.kind === 'task')
|
|
83
|
+
live.add('note', out.state === 'started' ? `Started in the background: ${out.text}` : `Background work ${out.state}: ${out.text}`);
|
|
84
|
+
else if (out.kind === 'done')
|
|
85
|
+
live.idle();
|
|
86
|
+
},
|
|
87
|
+
turnEnded: () => void live?.end(),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=chat.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chat.js","sourceRoot":"","sources":["../../src/features/chat.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `dispatch` feature (a team lead): noticing that a worker is free, and deciding whether that
|
|
3
|
+
* is worth a model turn. Waking the lead's model costs a turn on a large context; waking it to
|
|
4
|
+
* learn there is nothing to hand out is the waste this exists to prevent.
|
|
5
|
+
* Split out of serve.ts (docs/spec-harness-profiles.md, step 2).
|
|
6
|
+
*/
|
|
7
|
+
import type { StreamEvent } from '../stream.js';
|
|
8
|
+
import { type Team } from '../team.js';
|
|
9
|
+
/**
|
|
10
|
+
* A worker saying it is free, recognised by the harness rather than left to the brief.
|
|
11
|
+
*
|
|
12
|
+
* The message is one the harness writes (see offerToLead in serve.ts), so its shape is known and
|
|
13
|
+
* matching it is not guesswork. It is turned into an instruction on the turn it arrives, because a
|
|
14
|
+
* standing rule in a brief is read once at the start of a long session and gets skimmed past — and
|
|
15
|
+
* the cost of skimming this one is a worker sitting idle beside a full backlog.
|
|
16
|
+
*/
|
|
17
|
+
export declare const FREE_WORKER: RegExp;
|
|
18
|
+
export declare function isFreeWorkerMessage(event: Pick<StreamEvent, 'event' | 'message' | 'line'>): boolean;
|
|
19
|
+
/** What the lead's turn is told when a worker says it is free. Empty for any other event. */
|
|
20
|
+
export declare function freeWorkerBrief(event: StreamEvent): string;
|
|
21
|
+
/**
|
|
22
|
+
* The lead's reports that are free. Not 'never-seen': a member that has never connected (FRIDAY is
|
|
23
|
+
* a person's own session) is not a free worker, and counting one kept the team permanently "idle".
|
|
24
|
+
*/
|
|
25
|
+
export declare function idleWorkers(team: Team): string[];
|
|
26
|
+
/**
|
|
27
|
+
* How often an idle worker may wake the lead. The lead polls its team every minute to draw the
|
|
28
|
+
* pane, so it knows within a minute — but a worker with genuinely nothing to give it stays idle, and
|
|
29
|
+
* a lead asked about it every minute learns to say "nothing" without looking.
|
|
30
|
+
*/
|
|
31
|
+
export declare const DISPATCH_AGAIN_MS: number;
|
|
32
|
+
/** The same ready cards already looked at for a free worker are looked at again after this long. */
|
|
33
|
+
export declare const FREE_LOOK_AGAIN_MS: number;
|
|
34
|
+
export type DispatchDeps = {
|
|
35
|
+
/** Card codes ready to hand out; ['?'] when unreadable, so a caller still wakes. */
|
|
36
|
+
ready: () => Promise<string[]>;
|
|
37
|
+
log: (kind: 'tool' | 'event', text: string) => void;
|
|
38
|
+
now?: () => number;
|
|
39
|
+
};
|
|
40
|
+
export declare function createDispatch(d: DispatchDeps): {
|
|
41
|
+
/**
|
|
42
|
+
* The team was just read: should the lead be woken to hand out work? Returns the line for the
|
|
43
|
+
* sweep event that wakes it, or null. Eleven cards waited in Ready For Dev while two agents sat
|
|
44
|
+
* idle and the lead reported "waiting for work", before this.
|
|
45
|
+
*/
|
|
46
|
+
onTeam(team: Team, busy: boolean): Promise<string | null>;
|
|
47
|
+
/**
|
|
48
|
+
* A worker said it is free: is there anything new to give it? With nothing ready, or the very
|
|
49
|
+
* cards the lead already looked at and chose to hold, the lead's model would only re-read the
|
|
50
|
+
* board and say "nothing" — R2-D2 was woken five times for exactly that after a restart, and
|
|
51
|
+
* JARVIS re-read the same five cards for every "I am free". Looked at again when the set
|
|
52
|
+
* changes, or after an hour in case whatever held them is gone.
|
|
53
|
+
*/
|
|
54
|
+
wakeForFree(event: Pick<StreamEvent, "actor">): Promise<boolean>;
|
|
55
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { dispatchDecision, myTeam } from '../team.js';
|
|
2
|
+
/**
|
|
3
|
+
* A worker saying it is free, recognised by the harness rather than left to the brief.
|
|
4
|
+
*
|
|
5
|
+
* The message is one the harness writes (see offerToLead in serve.ts), so its shape is known and
|
|
6
|
+
* matching it is not guesswork. It is turned into an instruction on the turn it arrives, because a
|
|
7
|
+
* standing rule in a brief is read once at the start of a long session and gets skimmed past — and
|
|
8
|
+
* the cost of skimming this one is a worker sitting idle beside a full backlog.
|
|
9
|
+
*/
|
|
10
|
+
export const FREE_WORKER = /(^|\s)(Done with [A-Z][A-Z0-9]*-\d+\.|I am free — nothing queued, nothing running\.)\s*What is next\?/;
|
|
11
|
+
export function isFreeWorkerMessage(event) {
|
|
12
|
+
return (event.event === 'message' || event.event === 'mentioned') && FREE_WORKER.test(`${event.message ?? ''} ${event.line ?? ''}`);
|
|
13
|
+
}
|
|
14
|
+
/** What the lead's turn is told when a worker says it is free. Empty for any other event. */
|
|
15
|
+
export function freeWorkerBrief(event) {
|
|
16
|
+
if (!isFreeWorkerMessage(event))
|
|
17
|
+
return '';
|
|
18
|
+
const who = event.actor || 'A worker';
|
|
19
|
+
return (`\n\n## ${who} has nothing to do\n\n` +
|
|
20
|
+
`That message is the harness telling you this agent's queue is empty and nothing is running ` +
|
|
21
|
+
`on its machine. It is not a status update and it does not need a reply — it needs a card.\n\n` +
|
|
22
|
+
`Find it work now, before anything else in this turn:\n` +
|
|
23
|
+
` · \`get-portfolio\` with state "todo" — for you it lists the board's "Start work here" lanes ` +
|
|
24
|
+
`AND your pool ("Lead picks from here", usually the Backlog). Your pool is yours to distribute: ` +
|
|
25
|
+
`move a card from it into a "Start work here" lane, or assign it, by who is free. Workers can also ` +
|
|
26
|
+
`pull from "Start work here" themselves. Never move a card out of any OTHER not-started lane ` +
|
|
27
|
+
`(Discovery, a plain Backlog, anything hidden) — what leaves those is a person's decision. A board ` +
|
|
28
|
+
`with neither kind of lane has no lane strategy yet: start nothing there, and tell your human.\n` +
|
|
29
|
+
` · \`list_agents\` for what the rest of the team already holds, so you hand it something ` +
|
|
30
|
+
`nobody else is part-way through.\n` +
|
|
31
|
+
` · Pick something it can actually run: a repository it has, on a machine you believe is ` +
|
|
32
|
+
`working.\n` +
|
|
33
|
+
` · Assign it with \`assign-card\`. That is what wakes it.\n\n` +
|
|
34
|
+
`If there is genuinely nothing for it, say so in one line and leave it idle. That is a real ` +
|
|
35
|
+
`answer.\n`);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The lead's reports that are free. Not 'never-seen': a member that has never connected (FRIDAY is
|
|
39
|
+
* a person's own session) is not a free worker, and counting one kept the team permanently "idle".
|
|
40
|
+
*/
|
|
41
|
+
export function idleWorkers(team) {
|
|
42
|
+
return myTeam(team).mine.filter((a) => !a.card && a.state !== 'stopped' && a.state !== 'never-seen' && !a.stale).map((a) => a.name);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* How often an idle worker may wake the lead. The lead polls its team every minute to draw the
|
|
46
|
+
* pane, so it knows within a minute — but a worker with genuinely nothing to give it stays idle, and
|
|
47
|
+
* a lead asked about it every minute learns to say "nothing" without looking.
|
|
48
|
+
*/
|
|
49
|
+
export const DISPATCH_AGAIN_MS = 10 * 60 * 1000;
|
|
50
|
+
/** The same ready cards already looked at for a free worker are looked at again after this long. */
|
|
51
|
+
export const FREE_LOOK_AGAIN_MS = 60 * 60_000;
|
|
52
|
+
export function createDispatch(d) {
|
|
53
|
+
const now = d.now ?? Date.now;
|
|
54
|
+
let lastDispatch = 0;
|
|
55
|
+
let lastDispatchKey = '';
|
|
56
|
+
let lastSkipWhy = '';
|
|
57
|
+
let lastFreeLook = { key: '', at: 0 };
|
|
58
|
+
return {
|
|
59
|
+
/**
|
|
60
|
+
* The team was just read: should the lead be woken to hand out work? Returns the line for the
|
|
61
|
+
* sweep event that wakes it, or null. Eleven cards waited in Ready For Dev while two agents sat
|
|
62
|
+
* idle and the lead reported "waiting for work", before this.
|
|
63
|
+
*/
|
|
64
|
+
async onTeam(team, busy) {
|
|
65
|
+
const free = idleWorkers(team);
|
|
66
|
+
if (!free.length || busy || now() - lastDispatch <= DISPATCH_AGAIN_MS)
|
|
67
|
+
return null;
|
|
68
|
+
lastDispatch = now();
|
|
69
|
+
const decision = dispatchDecision(lastDispatchKey, await d.ready(), free);
|
|
70
|
+
lastDispatchKey = decision.key;
|
|
71
|
+
if (!decision.wake) {
|
|
72
|
+
if (decision.why !== lastSkipWhy)
|
|
73
|
+
d.log('tool', `${free.join(', ')} idle — not waking the lead: ${decision.why}`);
|
|
74
|
+
lastSkipWhy = decision.why;
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
lastSkipWhy = '';
|
|
78
|
+
d.log('event', `${free.join(', ')} ${free.length === 1 ? 'is' : 'are'} idle — looking for work to hand over`);
|
|
79
|
+
return `${free.join(', ')} have nothing to do`;
|
|
80
|
+
},
|
|
81
|
+
/**
|
|
82
|
+
* A worker said it is free: is there anything new to give it? With nothing ready, or the very
|
|
83
|
+
* cards the lead already looked at and chose to hold, the lead's model would only re-read the
|
|
84
|
+
* board and say "nothing" — R2-D2 was woken five times for exactly that after a restart, and
|
|
85
|
+
* JARVIS re-read the same five cards for every "I am free". Looked at again when the set
|
|
86
|
+
* changes, or after an hour in case whatever held them is gone.
|
|
87
|
+
*/
|
|
88
|
+
async wakeForFree(event) {
|
|
89
|
+
const ready = await d.ready();
|
|
90
|
+
const key = [...ready].sort().join(',');
|
|
91
|
+
const unchanged = !!key && key === lastFreeLook.key && now() - lastFreeLook.at < FREE_LOOK_AGAIN_MS;
|
|
92
|
+
if (!ready.length || unchanged) {
|
|
93
|
+
d.log('tool', `${event.actor || 'a worker'} is free — ${ready.length ? 'the ready cards are the ones already looked at' : 'nothing ready to hand over'}, not waking the model`);
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
lastFreeLook = { key, at: now() };
|
|
97
|
+
return true;
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=dispatch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dispatch.js","sourceRoot":"","sources":["../../src/features/dispatch.ts"],"names":[],"mappings":"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"}
|