@passioncode-ai/passioncode 0.1.53 → 0.2.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/CHANGELOG.md +41 -0
- package/README.md +72 -23
- package/SECURITY.md +23 -3
- package/bin/passioncode.js +71 -6
- package/family.json +2 -2
- package/lib/catalog.js +91 -0
- package/lib/doctor.js +288 -0
- package/lib/launcher.js +123 -58
- package/lib/mcp-probe.js +91 -0
- package/lib/openpgp.js +209 -0
- package/lib/release-key.asc +10 -0
- package/lib/releases.js +211 -0
- package/lib/setup.js +238 -0
- package/package.json +6 -3
- package/payload/.claude-plugin/marketplace.json +3 -3
- package/payload/manifest.json +10 -9
- package/payload/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +3 -2
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +5 -5
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +26 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +182 -0
- package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +1 -1
- package/payload/plugins/passioncode/.claude-plugin/plugin.json +2 -2
- package/payload/plugins/passioncode/hooks/session-start.js +13 -1
- package/payload/plugins/passioncode/skills/setting-up-passioncode/SKILL.md +109 -0
package/lib/setup.js
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* `passioncode setup` (alias `onboard`): one idempotent command for a new machine and an existing
|
|
4
|
+
* setup (ADR-0004). It checks everything first (the doctor's checks), plans, asks, then:
|
|
5
|
+
* installs the absent apps the person chose — verified releases only;
|
|
6
|
+
* repairs what the checks found — the skill set and MCP registrations (`update`), an outdated or
|
|
7
|
+
* damaged app nobody is running (a verified reinstall; the old bundle goes to the Trash), a
|
|
8
|
+
* launchd service that does not answer (`launchctl kickstart`, never a kill);
|
|
9
|
+
* opens the apps it installed;
|
|
10
|
+
* then checks again and prints the before/after table and the steps that stay a person's.
|
|
11
|
+
* On a healthy machine the plan is empty, nothing changes, and it exits 0.
|
|
12
|
+
* `doctor --fix` is this command with `repairOnly`: no new apps, nothing opened.
|
|
13
|
+
*/
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const os = require('os');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const L = require('./launcher');
|
|
18
|
+
const catalog = require('./catalog');
|
|
19
|
+
const releases = require('./releases');
|
|
20
|
+
const D = require('./doctor');
|
|
21
|
+
|
|
22
|
+
function prereqMissing(product) {
|
|
23
|
+
const missing = [];
|
|
24
|
+
for (const need of product.needs || []) {
|
|
25
|
+
if (need === 'docker' && releases.run('docker', ['--version'], { timeout: 10000 }).code === 127) missing.push('Docker (OrbStack or Docker Desktop)');
|
|
26
|
+
if (need === 'supabase' && releases.run('supabase', ['--version'], { timeout: 10000 }).code === 127) missing.push('the Supabase CLI');
|
|
27
|
+
}
|
|
28
|
+
return missing;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** What setup would do, from the checks: installs of absent apps and repairs. */
|
|
32
|
+
function plan(checks, { platform, withIds = null, withoutIds = [], noApps = false, repairOnly = false }) {
|
|
33
|
+
const items = [];
|
|
34
|
+
if (platform === 'darwin' && !noApps && !repairOnly) {
|
|
35
|
+
for (const pr of catalog.PRODUCTS) {
|
|
36
|
+
if (fs.existsSync(catalog.appPath(pr))) continue;
|
|
37
|
+
if (withoutIds.includes(pr.id) || (withIds && !withIds.includes(pr.id))) continue;
|
|
38
|
+
const missing = prereqMissing(pr);
|
|
39
|
+
const explicit = !!(withIds && withIds.includes(pr.id));
|
|
40
|
+
items.push({
|
|
41
|
+
kind: 'install', product: pr.id, selected: explicit || !missing.length,
|
|
42
|
+
detail: `install ${pr.name}${pr.preview ? ' (early preview)' : ''} from its latest signed release`,
|
|
43
|
+
note: missing.length ? `needs ${missing.join(' and ')}${explicit ? '; installed because you asked for it' : '; skipped unless you ask for it (--with ' + pr.id + ')'}` : null,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const seen = new Set();
|
|
48
|
+
for (const c of checks) {
|
|
49
|
+
const r = c.repair;
|
|
50
|
+
if (!r) continue;
|
|
51
|
+
if (r.kind === 'replace' && (noApps || platform !== 'darwin')) continue;
|
|
52
|
+
if (r.kind === 'replace' && (withoutIds.includes(r.product) || (withIds && !withIds.includes(r.product)))) continue;
|
|
53
|
+
const key = r.kind === 'update' ? 'update' : `${r.kind}:${r.product || r.label}`;
|
|
54
|
+
if (seen.has(key)) { items.find((i) => i.key === key).because.push(c.id); continue; }
|
|
55
|
+
seen.add(key);
|
|
56
|
+
const pr = r.product ? catalog.byId(r.product) : null;
|
|
57
|
+
items.push({
|
|
58
|
+
key, kind: r.kind, product: r.product || null, label: r.label || null, selected: true, because: [c.id],
|
|
59
|
+
detail: r.kind === 'update' ? 'install or refresh the skill set and register the apps\' MCP servers for every agent'
|
|
60
|
+
: r.kind === 'replace' ? `reinstall ${pr.name} from its latest signed release (the current one goes to the Trash)`
|
|
61
|
+
: `start the ${r.label} service through launchd`,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return items;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const yesNo = async (ask, question, def) => {
|
|
68
|
+
const a = String(await ask(`${question} [${def ? 'Y/n' : 'y/N'}] `) || '').trim().toLowerCase();
|
|
69
|
+
return a ? a.startsWith('y') || a.startsWith('д') : def;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
async function setup({
|
|
73
|
+
home = os.homedir(), yes = false, withIds = null, withoutIds = [], noApps = false, noOpen = false, dryRun = false,
|
|
74
|
+
repairOnly = false, online = true, ask = null, log = () => {}, platform = process.env.PASSIONCODE_PLATFORM || process.platform,
|
|
75
|
+
} = {}) {
|
|
76
|
+
const claude = L.claudeRunner();
|
|
77
|
+
const before = D.sortChecks(await D.collect({ home, platform, claude, online }));
|
|
78
|
+
const fresh = !before.some((c) => c.id === 'skills.installed' && c.state !== 'fail')
|
|
79
|
+
&& !catalog.PRODUCTS.some((pr) => platform === 'darwin' && fs.existsSync(catalog.appPath(pr)));
|
|
80
|
+
const items = plan(before, { platform, withIds, withoutIds, noApps, repairOnly });
|
|
81
|
+
const result = {
|
|
82
|
+
version: D.PKG, platform, mode: fresh ? 'new' : 'existing', at: new Date().toISOString(), plan: items,
|
|
83
|
+
actions: [], before: { checks: before, summary: D.summarise(before) }, after: null, humanSteps: [], planOnly: false,
|
|
84
|
+
keyOverride: process.env.PASSIONCODE_RELEASE_KEY || null,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// Ask: one question per absent app, one for the repairs. Without a terminal and without --yes,
|
|
88
|
+
// nothing changes: the plan is the answer.
|
|
89
|
+
if (dryRun) { result.planOnly = true; return finish(result, home, before); }
|
|
90
|
+
if (!yes) {
|
|
91
|
+
if (!ask) { result.planOnly = items.some((i) => i.selected); if (result.planOnly) return finish(result, home, before); }
|
|
92
|
+
else {
|
|
93
|
+
for (const i of items.filter((x) => x.kind === 'install')) {
|
|
94
|
+
const pr = catalog.byId(i.product);
|
|
95
|
+
i.selected = await yesNo(ask, `Install ${pr.name}${pr.preview ? ' (early preview)' : ''}${i.note ? ` — ${i.note}` : ''}?`, i.selected);
|
|
96
|
+
}
|
|
97
|
+
const repairs = items.filter((x) => x.kind !== 'install');
|
|
98
|
+
if (repairs.length) {
|
|
99
|
+
const go = await yesNo(ask, `Apply ${repairs.length} repair(s): ${repairs.map((r) => r.detail).join('; ')}?`, true);
|
|
100
|
+
for (const r of repairs) r.selected = go;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const act = (kind, detail, product = null) => {
|
|
106
|
+
const a = { kind, detail, product, outcome: 'done' };
|
|
107
|
+
result.actions.push(a);
|
|
108
|
+
log(`${kind.padEnd(9)} ${detail}`);
|
|
109
|
+
return a;
|
|
110
|
+
};
|
|
111
|
+
const fail = (a, error) => { a.outcome = 'failed'; a.error = error.message || String(error); log(` failed: ${a.error}`); };
|
|
112
|
+
const installed = [];
|
|
113
|
+
let needUpdate = items.some((i) => i.kind === 'update' && i.selected);
|
|
114
|
+
|
|
115
|
+
// 1. Apps: installs of absent ones, verified reinstalls of outdated or damaged ones.
|
|
116
|
+
for (const i of items.filter((x) => (x.kind === 'install' || x.kind === 'replace') && x.selected)) {
|
|
117
|
+
const pr = catalog.byId(i.product);
|
|
118
|
+
const a = act(i.kind, i.detail, pr.id);
|
|
119
|
+
try {
|
|
120
|
+
const release = await releases.latestRelease(pr, { cacheFile: path.join(L.paths(home).state, 'releases-cache.json') });
|
|
121
|
+
const done = await releases.installApp(pr, release, {
|
|
122
|
+
work: path.join(L.paths(home).state, 'downloads', `${pr.id}-${release.version}`), replace: i.kind === 'replace', home,
|
|
123
|
+
report: (st) => log(` ${st.ok === false ? 'FAILED' : 'ok'} ${st.detail}`),
|
|
124
|
+
});
|
|
125
|
+
a.detail = `${i.kind === 'install' ? 'installed' : 'reinstalled'} ${pr.name} ${done.version} at ${done.path}${done.replaced ? `; the previous one is in ${done.replaced}` : ''}`;
|
|
126
|
+
if (i.kind === 'install') installed.push(pr); // a reinstalled app was idle and stays closed
|
|
127
|
+
needUpdate = needUpdate || (i.kind === 'install' && !!(pr.mcp && !pr.mcp.external));
|
|
128
|
+
} catch (error) { fail(a, error); }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 2. The skill set and the bundled MCP registrations.
|
|
132
|
+
if (needUpdate) {
|
|
133
|
+
const a = act('update', 'the skill set and the apps\' MCP registrations');
|
|
134
|
+
try {
|
|
135
|
+
const r = L.update({ home, claude, includePreview: true }); // setup is where a person chose Fabric
|
|
136
|
+
const failed = r.steps.filter((st) => st.outcome === 'failed');
|
|
137
|
+
const changed = r.steps.filter((st) => st.outcome === 'done');
|
|
138
|
+
a.steps = changed.concat(failed);
|
|
139
|
+
a.detail = `PassionCode.ai ${r.version}: ${changed.length} change(s)${failed.length ? `, ${failed.length} failed` : ''}`;
|
|
140
|
+
if (failed.length) fail(a, new Error(failed.map((st) => `${st.detail}: ${st.error}`).join('; ')));
|
|
141
|
+
} catch (error) { fail(a, error); }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 3. Services launchd owns and that did not answer: start, never kill.
|
|
145
|
+
for (const i of items.filter((x) => x.kind === 'kickstart' && x.selected)) {
|
|
146
|
+
const a = act('start', `launchctl kickstart gui/${process.getuid ? process.getuid() : 0}/${i.label}`);
|
|
147
|
+
const r = releases.run('launchctl', ['kickstart', `gui/${process.getuid ? process.getuid() : 0}/${i.label}`], { timeout: 30000 });
|
|
148
|
+
if (r.code !== 0) fail(a, new Error((r.err || r.out || `exit ${r.code}`).trim()));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 4. Open what was installed for its first run (never in --fix, never with --no-open).
|
|
152
|
+
if (!repairOnly && !noOpen) {
|
|
153
|
+
for (const pr of installed) {
|
|
154
|
+
const a = act('open', `${pr.name} for its first run`, pr.id);
|
|
155
|
+
const r = releases.run('open', [catalog.appPath(pr)], { timeout: 30000 });
|
|
156
|
+
if (r.code !== 0) fail(a, new Error((r.err || r.out || `exit ${r.code}`).trim()));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const after = result.actions.length ? D.sortChecks(await D.collect({ home, platform, claude, online })) : before;
|
|
161
|
+
result.after = { checks: after, summary: D.summarise(after) };
|
|
162
|
+
|
|
163
|
+
// The person's steps, once: every remaining problem's fix, the guides of the apps just installed,
|
|
164
|
+
// what an install leaves to a person, and a restart when skills or MCP servers changed.
|
|
165
|
+
const steps = [];
|
|
166
|
+
const push = (t) => { if (t && !steps.includes(t)) steps.push(t); };
|
|
167
|
+
const anyFailed = result.actions.some((x) => x.outcome === 'failed');
|
|
168
|
+
for (const c of after) {
|
|
169
|
+
if (!(c.state === 'fail' || c.state === 'warn') || !c.fix) continue;
|
|
170
|
+
if (c.fix === `${D.NPX} setup` && !anyFailed && !result.planOnly) continue; // this run was that command
|
|
171
|
+
push(c.fix);
|
|
172
|
+
}
|
|
173
|
+
for (const i of items.filter((x) => x.kind === 'install' && !x.selected)) {
|
|
174
|
+
const pr = catalog.byId(i.product);
|
|
175
|
+
if (i.note) push(`${pr.name}: ${i.note}`);
|
|
176
|
+
}
|
|
177
|
+
for (const pr of installed) push(pr.guide);
|
|
178
|
+
if (result.actions.some((a) => a.kind === 'update' && a.outcome === 'done' && /[1-9]\d* change/.test(a.detail)) || installed.length) {
|
|
179
|
+
push('Restart your coding agents: skills and MCP servers load when a session starts.');
|
|
180
|
+
}
|
|
181
|
+
result.humanSteps = steps;
|
|
182
|
+
return finish(result, home, after);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function finish(result, home, checks) {
|
|
186
|
+
result.ok = !result.actions.some((a) => a.outcome === 'failed') && D.summarise(checks).fail === 0;
|
|
187
|
+
if (!result.after) result.after = { checks, summary: D.summarise(checks) };
|
|
188
|
+
// Stamped after the after-check: the update inside this run set installedAt earlier, and the
|
|
189
|
+
// session hook ignores a result older than the installed release.
|
|
190
|
+
if (!result.planOnly) D.save(home, { at: new Date().toISOString(), checks, summary: D.summarise(checks) });
|
|
191
|
+
return result;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function render(result, { repairOnly = false } = {}) {
|
|
195
|
+
const lines = [];
|
|
196
|
+
if (result.keyOverride) lines.push(`NOTE: releases are checked against the key in PASSIONCODE_RELEASE_KEY (${result.keyOverride}), not the bundled PassionCode.ai release key.`, '');
|
|
197
|
+
const b = result.before.summary, a = result.after.summary;
|
|
198
|
+
lines.push(`${repairOnly ? 'doctor --fix' : 'setup'} · ${result.mode === 'new' ? 'a new setup' : 'an existing setup'} · ${result.platform}`);
|
|
199
|
+
const todo = result.plan.filter((i) => i.selected);
|
|
200
|
+
if (result.planOnly) {
|
|
201
|
+
lines.push('', todo.length ? 'Plan (nothing was changed):' : 'Nothing to do.');
|
|
202
|
+
for (const i of result.plan) lines.push(` ${i.selected ? '•' : '–'} ${i.detail}${i.note ? ` (${i.note})` : ''}`);
|
|
203
|
+
if (todo.length) lines.push('', 'Run it with --yes to apply, or in a terminal to answer one question per app.');
|
|
204
|
+
} else if (!result.actions.length) {
|
|
205
|
+
lines.push('', 'Everything this command can set up is in place; nothing was changed.');
|
|
206
|
+
} else {
|
|
207
|
+
lines.push('', 'Done:');
|
|
208
|
+
for (const x of result.actions) lines.push(` ${x.outcome === 'failed' ? 'FAILED' : 'done '} ${x.detail}${x.error ? `\n ${x.error}` : ''}`);
|
|
209
|
+
}
|
|
210
|
+
// Before → after, by area, and every check whose state moved.
|
|
211
|
+
const areas = ['prerequisites', 'skills', 'apps', 'mcp', 'services'];
|
|
212
|
+
const count = (checks, area, st) => checks.filter((c) => c.area === area && c.state === st).length;
|
|
213
|
+
const single = result.planOnly || !result.actions.length;
|
|
214
|
+
lines.push('', single ? 'Checks now (ok/warn/fail)' : 'Checks before (ok/warn/fail) after (ok/warn/fail)');
|
|
215
|
+
for (const area of areas) {
|
|
216
|
+
const bc = result.before.checks, ac = result.after.checks;
|
|
217
|
+
if (!bc.some((c) => c.area === area) && !ac.some((c) => c.area === area)) continue;
|
|
218
|
+
const f = (cs) => `${count(cs, area, 'ok')}/${count(cs, area, 'warn')}/${count(cs, area, 'fail')}`;
|
|
219
|
+
lines.push(single ? ` ${area.padEnd(14)}${f(ac)}` : ` ${area.padEnd(14)}${f(bc).padEnd(24)}${f(ac)}`);
|
|
220
|
+
}
|
|
221
|
+
const was = new Map(result.before.checks.map((c) => [c.id, c.state]));
|
|
222
|
+
const moved = result.after.checks.filter((c) => was.has(c.id) && was.get(c.id) !== c.state && c.state !== 'info');
|
|
223
|
+
for (const c of moved) lines.push(` ${was.get(c.id)} → ${c.state}: ${c.detail}`);
|
|
224
|
+
const left = result.after.checks.filter((c) => c.state === 'fail' || c.state === 'warn');
|
|
225
|
+
if (left.length) {
|
|
226
|
+
lines.push('', result.planOnly ? 'Open now:' : 'Still open:');
|
|
227
|
+
for (const c of left) lines.push(` ${c.state === 'fail' ? 'FAIL' : 'warn'} ${c.detail}`);
|
|
228
|
+
}
|
|
229
|
+
if (result.humanSteps.length) {
|
|
230
|
+
lines.push('', 'Your steps (an agent cannot do these for you — sign-ins, consents, secrets in hidden prompts):');
|
|
231
|
+
result.humanSteps.forEach((t, n) => lines.push(` ${n + 1}. ${t}`));
|
|
232
|
+
}
|
|
233
|
+
lines.push('', single ? `${a.fail ? `${a.fail} problem(s)` : 'No problems'}. Check again any time: npx @passioncode-ai/passioncode@latest doctor`
|
|
234
|
+
: `${a.fail ? `${a.fail} problem(s) remain` : 'No problems remain'} (before: ${b.fail}). Check again any time: npx @passioncode-ai/passioncode@latest doctor`);
|
|
235
|
+
return lines.join('\n');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
module.exports = { setup, plan, render };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@passioncode-ai/passioncode",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Set up PassionCode.ai on a computer with one command — the apps from their verified releases, their MCP servers for every coding agent and the PassionCode.ai skills — and check and repair it with the same command or `doctor`.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"passioncode": "bin/passioncode.js"
|
|
7
7
|
},
|
|
@@ -45,6 +45,9 @@
|
|
|
45
45
|
"passioncode",
|
|
46
46
|
"fabric",
|
|
47
47
|
"skills",
|
|
48
|
-
"launcher"
|
|
48
|
+
"launcher",
|
|
49
|
+
"setup",
|
|
50
|
+
"doctor",
|
|
51
|
+
"mcp"
|
|
49
52
|
]
|
|
50
53
|
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"name": "fabric-agent-adapter",
|
|
12
12
|
"displayName": "Fabric Agent Adapter",
|
|
13
13
|
"source": "./plugins/fabric-agent-adapter",
|
|
14
|
-
"version": "0.
|
|
14
|
+
"version": "0.30.0",
|
|
15
15
|
"description": "Create new Fabric-compatible agents, build them as always-alive local services with dashboards (fabric-service/0.1), and adapt existing projects to the Fabric Agent Contract through evidence-backed MCP, A2A, or local-runner provider bundles; and teach a coding agent to work in a project through Fabric (fabric-mcp, fabric_whoami, claim and release), with Claude Code session hooks.",
|
|
16
16
|
"author": {
|
|
17
17
|
"name": "PassionCode.ai",
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
"name": "passioncode",
|
|
36
36
|
"displayName": "PassionCode.ai",
|
|
37
37
|
"source": "./plugins/passioncode",
|
|
38
|
-
"version": "0.1
|
|
39
|
-
"description": "
|
|
38
|
+
"version": "0.2.1",
|
|
39
|
+
"description": "Set up and check PassionCode.ai on a computer with one command (setting-up-passioncode), the organisation's working rules for every contributor's agent (working-in-passioncode), and a once-a-day check that keeps the PassionCode.ai set current.",
|
|
40
40
|
"author": {
|
|
41
41
|
"name": "PassionCode.ai",
|
|
42
42
|
"url": "https://passioncode.ai/"
|
package/payload/manifest.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"family": "passioncode",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"release": true,
|
|
5
5
|
"members": [
|
|
6
6
|
{
|
|
7
7
|
"name": "fabric-agent-adapter",
|
|
8
8
|
"displayName": "Fabric Agent Adapter",
|
|
9
9
|
"repo": "passioncode-ai/fabric-agent-adapter",
|
|
10
|
-
"ref": "v0.
|
|
11
|
-
"commit": "
|
|
12
|
-
"version": "0.
|
|
10
|
+
"ref": "v0.30.0",
|
|
11
|
+
"commit": "668b934ae92b1b52356f250e7dd7c3131d81842c",
|
|
12
|
+
"version": "0.30.0",
|
|
13
13
|
"via": "clone of https://***@github.com/passioncode-ai/fabric-agent-adapter.git",
|
|
14
14
|
"skills": [
|
|
15
15
|
"adapting-projects-to-fabric",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"legacyMarketplaces": [
|
|
24
24
|
"fabric-agent-adapter"
|
|
25
25
|
],
|
|
26
|
-
"contentHash": "sha256:
|
|
26
|
+
"contentHash": "sha256:7d270382683711b72cfb4a6ce0ddfbe28612a5f6d71ffe978c10ce96adad019f",
|
|
27
27
|
"description": "Create new Fabric-compatible agents, build them as always-alive local services with dashboards (fabric-service/0.1), and adapt existing projects to the Fabric Agent Contract through evidence-backed MCP, A2A, or local-runner provider bundles; and teach a coding agent to work in a project through Fabric (fabric-mcp, fabric_whoami, claim and release), with Claude Code session hooks.",
|
|
28
28
|
"author": {
|
|
29
29
|
"name": "PassionCode.ai",
|
|
@@ -62,17 +62,18 @@
|
|
|
62
62
|
"name": "passioncode",
|
|
63
63
|
"displayName": "PassionCode.ai",
|
|
64
64
|
"repo": "passioncode-ai/passioncode",
|
|
65
|
-
"ref": "v0.1
|
|
65
|
+
"ref": "v0.2.1",
|
|
66
66
|
"commit": null,
|
|
67
|
-
"version": "0.1
|
|
67
|
+
"version": "0.2.1",
|
|
68
68
|
"via": "this repository",
|
|
69
69
|
"skills": [
|
|
70
|
+
"setting-up-passioncode",
|
|
70
71
|
"working-in-passioncode"
|
|
71
72
|
],
|
|
72
73
|
"legacyPluginIds": [],
|
|
73
74
|
"legacyMarketplaces": [],
|
|
74
|
-
"contentHash": "sha256:
|
|
75
|
-
"description": "
|
|
75
|
+
"contentHash": "sha256:d982253f26d5d1135cb02afc64465d1ade106485f3a79bacfdfd6db4ff4f4965",
|
|
76
|
+
"description": "Set up and check PassionCode.ai on a computer with one command (setting-up-passioncode), the organisation's working rules for every contributor's agent (working-in-passioncode), and a once-a-day check that keeps the PassionCode.ai set current.",
|
|
76
77
|
"author": {
|
|
77
78
|
"name": "PassionCode.ai",
|
|
78
79
|
"url": "https://passioncode.ai/"
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "fabric-agent-adapter",
|
|
4
4
|
"displayName": "Fabric Agent Adapter",
|
|
5
5
|
"description": "Create new Fabric-compatible agents, build them as always-alive local services with dashboards (fabric-service/0.1), and adapt existing projects to the Fabric Agent Contract through evidence-backed MCP, A2A, or local-runner provider bundles; and teach a coding agent to work in a project through Fabric (fabric-mcp, fabric_whoami, claim and release), with Claude Code session hooks.",
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.30.0",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "PassionCode.ai",
|
|
9
9
|
"url": "https://passioncode.ai/"
|
|
@@ -5,7 +5,7 @@ license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
|
5
5
|
compatibility: Requires filesystem access and Python 3.9+. Exact schema checks additionally need git, Node.js, pnpm, and the pinned fabric-agent-contract checkout (a public repository). Works without those tools in an explicitly degraded structural-check mode.
|
|
6
6
|
metadata:
|
|
7
7
|
author: PassionCode.ai
|
|
8
|
-
version: "0.
|
|
8
|
+
version: "0.30.0"
|
|
9
9
|
contract-version: "0.1.0"
|
|
10
10
|
contract-commit: "f00a7685e3861c27f7899c033fb850a145a1708f"
|
|
11
11
|
---
|
|
@@ -17,7 +17,7 @@ license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
|
17
17
|
compatibility: Python 3.9+ or Node.js 20+ for the kits; the probe needs Python 3.9+. launchd steps are macOS-only; Linux services install a systemd user unit and Windows ones a per-user Scheduled Task with the kits' writers. Dashboard handoff optionally uses Fabric Dashboards MCP link/host_status/open; without it, report unresolved host capability. The contract checkout is optional.
|
|
18
18
|
metadata:
|
|
19
19
|
author: PassionCode.ai
|
|
20
|
-
version: "0.
|
|
20
|
+
version: "0.30.0"
|
|
21
21
|
contract-version: "0.1.0"
|
|
22
22
|
extension: "fabric-service/0.1"
|
|
23
23
|
extension-commit: "f00a7685e3861c27f7899c033fb850a145a1708f"
|
|
@@ -203,7 +203,8 @@ The installer, not the service, owns the plist and the descriptor. Sequence:
|
|
|
203
203
|
|
|
204
204
|
On Linux the supervisor is a systemd user unit (`systemd_unit` + `systemd_install`), on
|
|
205
205
|
Windows a per-user Scheduled Task (`task_xml` + `task_install`); the Node kit has the same
|
|
206
|
-
writers (`systemdUnit`, `taskXml`, …) and
|
|
206
|
+
writers and installers (`launchdPlist` + `launchdInstall`, `systemdUnit`, `taskXml`, …) and
|
|
207
|
+
both kits write identical files.
|
|
207
208
|
|
|
208
209
|
Code runs from an immutable release directory; an upgrade writes a new release,
|
|
209
210
|
rewrites the plist and restarts. Read [the lifecycle reference](references/lifecycle.md)
|
package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md
CHANGED
|
@@ -12,7 +12,7 @@ wins and this page is the defect. The rule each section serves:
|
|
|
12
12
|
| LC-09 the footprint is declared | the descriptor and the plist, unit or task; the service's own `AGENTS.md` names its label (unit, task) and port | `write_descriptor`, `launchd_plist`, `systemd_unit` / `systemdUnit`, `task_xml` / `taskXml` |
|
|
13
13
|
| LC-11 previous release kept, older pruned | [Install, upgrade, uninstall](#install-upgrade-uninstall) | `prune_releases` / `pruneReleases` |
|
|
14
14
|
| LC-12 every file is bounded | [Logs](#logs) | `RotatingLog`, `cap_stdout_log` / `RotatingLog`, `capStdoutLog` |
|
|
15
|
-
| LC-14 operator intent wins; uninstall is symmetric | [Install, upgrade, uninstall](#install-upgrade-uninstall), [Linux and Windows](#linux-and-windows) | `launchd_install`, `launchd_uninstall`
|
|
15
|
+
| LC-14 operator intent wins; uninstall is symmetric | [Install, upgrade, uninstall](#install-upgrade-uninstall), [Linux and Windows](#linux-and-windows) | `launchd_install` / `launchdInstall`, `launchd_uninstall` / `launchdUninstall`; `systemd_install` / `systemdInstall`, `task_install` / `taskInstall` and their uninstalls |
|
|
16
16
|
| LC-15 builds clean up after themselves | release pruning runs inside the installer, not later | `prune_releases` / `pruneReleases` |
|
|
17
17
|
|
|
18
18
|
## Contents
|
|
@@ -26,7 +26,7 @@ wins and this page is the defect. The rule each section serves:
|
|
|
26
26
|
- [Being managed by a host lifecycle broker](#being-managed-by-a-host-lifecycle-broker)
|
|
27
27
|
- [Linux and Windows](#linux-and-windows)
|
|
28
28
|
|
|
29
|
-
## launchd plist (generate it with `launchd_plist`)
|
|
29
|
+
## launchd plist (generate it with `launchd_plist` or `launchdPlist`; both kits write the same bytes)
|
|
30
30
|
|
|
31
31
|
| Key | Value | Why |
|
|
32
32
|
|---|---|---|
|
|
@@ -37,7 +37,7 @@ wins and this page is the defect. The rule each section serves:
|
|
|
37
37
|
| `ThrottleInterval` | `10` | bounds a crash loop |
|
|
38
38
|
| `ExitTimeOut` | above the drain and its hard exit (15 s default; `Drain` ends by 10 s) | SIGKILL arrives after it, so it must never come first |
|
|
39
39
|
| `ProcessType` | `Standard` — never `Background`, and no `Nice` > 0 or `LowPriorityIO` | a host probes the service and agents call it while the Mac is busy; a background job is starved for tens of seconds under load and every host reports an outage that never happened (the probe's `lifecycle.priority` rule fails it) |
|
|
40
|
-
| `EnvironmentVariables` | `PATH` set explicitly; `*_FILE` paths to secrets; `FABRIC_SERVICE_SUPERVISOR=launchd` | never a secret value — the plist is world-readable in backups; the supervisor marker is how the lock knows to back off |
|
|
40
|
+
| `EnvironmentVariables` | `PATH` set explicitly; `*_FILE` paths to secrets; `FABRIC_SERVICE_SUPERVISOR=launchd` | never a secret value — the plist is world-readable in backups; the supervisor marker is how the lock knows to back off (the probe's `lifecycle.supervisor-marker` rule fails a plist without it: a copy that finds the lock held would exit 75 at once into a respawn every `ThrottleInterval`) |
|
|
41
41
|
| `StandardOutPath`/`StandardErrorPath` | `~/Library/Logs/<id>/service.log` | one place the host tails |
|
|
42
42
|
|
|
43
43
|
## Install, upgrade, uninstall
|
|
@@ -45,14 +45,14 @@ wins and this page is the defect. The rule each section serves:
|
|
|
45
45
|
1. Build or unpack the release into `~/.local/share/<id>/releases/<version>-<sha12>/`
|
|
46
46
|
(a venv or `node_modules` inside it). Never edit a release after it is written.
|
|
47
47
|
2. `write_descriptor` (port and id claims are checked here).
|
|
48
|
-
3. `launchd_install
|
|
48
|
+
3. `launchd_install` (Node: `await launchdInstall(...)`, the same steps): write + `plutil -lint`, read `launchctl print-disabled gui/<uid>`,
|
|
49
49
|
`enable` **only when no override is recorded** (a first install), `bootout` and wait
|
|
50
50
|
until `launchctl print gui/<uid>/<label>` fails, `bootstrap` with up to five retries,
|
|
51
51
|
then poll the well-known document until it answers with this `id.instance`.
|
|
52
52
|
4. Upgrade = new release, rewrite the plist with the new path, `launchd_install`
|
|
53
53
|
again, then `prune_releases(releases_dir, current=<new release>)`: the running release
|
|
54
54
|
and the one before it stay for rollback, older ones go (LC-11, LC-15).
|
|
55
|
-
5. Uninstall = `launchd_uninstall` + `remove_descriptor`. Data stays; `purge=True`
|
|
55
|
+
5. Uninstall = `launchd_uninstall` (Node: `await launchdUninstall(...)`) + `remove_descriptor`. Data stays; `purge=True`
|
|
56
56
|
(with `service_id`) removes data, logs and cache when the operator asks.
|
|
57
57
|
|
|
58
58
|
**The operator's intent wins (LC-14).** A host stops a service with `bootout` **and**
|
|
@@ -75,6 +75,29 @@ def plist_problems(plist: dict, label: object, token: object = None) -> list:
|
|
|
75
75
|
return problems
|
|
76
76
|
|
|
77
77
|
|
|
78
|
+
def marker_problem(plist: dict) -> Optional[str]:
|
|
79
|
+
"""What is wrong with the job's supervisor marker (DEC-0057; FAA-05), read the way the copy it starts reads it.
|
|
80
|
+
|
|
81
|
+
Without ``FABRIC_SERVICE_SUPERVISOR=launchd`` (or ``--fabric-supervisor launchd``) a copy that finds the
|
|
82
|
+
lock held exits 75 at once, and KeepAlive starts it again every ThrottleInterval."""
|
|
83
|
+
env = plist.get("EnvironmentVariables")
|
|
84
|
+
env = {str(k): str(v) for k, v in env.items()} if isinstance(env, dict) else {}
|
|
85
|
+
argv = plist.get("ProgramArguments")
|
|
86
|
+
argv = [str(a) for a in argv] if isinstance(argv, list) else []
|
|
87
|
+
marker = fs.supervisor_marker(env, argv)
|
|
88
|
+
if marker["kind"] == "supervised" and marker["manager"] == "launchd":
|
|
89
|
+
return None
|
|
90
|
+
fix = ("a copy that finds the lock held exits 75 at once and KeepAlive respawns it every ThrottleInterval; "
|
|
91
|
+
"regenerate the plist with launchd_plist / launchdPlist and reinstall")
|
|
92
|
+
if marker["kind"] == "conflict":
|
|
93
|
+
return "%s — %s" % (fs.marker_conflict_message(marker), fix)
|
|
94
|
+
if marker["kind"] == "supervised":
|
|
95
|
+
return "the marker names %s, not launchd — %s" % (marker["manager"], fix)
|
|
96
|
+
if marker.get("unreadable") is not None:
|
|
97
|
+
return "the marker %r names no supervisor — %s" % (marker["unreadable"], fix)
|
|
98
|
+
return "no %s=launchd — %s" % (fs.SUPERVISOR_ENV, fix)
|
|
99
|
+
|
|
100
|
+
|
|
78
101
|
def priority_problems(plist: dict) -> list:
|
|
79
102
|
"""A service that answers hosts and agents must not be scheduled as background work."""
|
|
80
103
|
out = []
|
|
@@ -701,6 +724,9 @@ class Probe:
|
|
|
701
724
|
return
|
|
702
725
|
problems = plist_problems(plist, life.get("label"), self.token)
|
|
703
726
|
self.add("lifecycle.plist", "FAIL" if problems else "PASS", "; ".join(problems) or "%s: RunAtLoad, KeepAlive, no secrets" % plist_path.name)
|
|
727
|
+
marker = marker_problem(plist)
|
|
728
|
+
self.add("lifecycle.supervisor-marker", "FAIL" if marker else "PASS",
|
|
729
|
+
marker or "%s: %s=launchd, so a copy that finds the lock held backs off" % (plist_path.name, fs.SUPERVISOR_ENV))
|
|
704
730
|
slow = priority_problems(plist)
|
|
705
731
|
self.add("lifecycle.priority", "FAIL" if slow else "PASS",
|
|
706
732
|
"; ".join(slow) + " — macOS may starve the service under load and hosts then see an outage; use ProcessType Standard"
|