@sabaiway/agent-workflow-kit 14.24.0 → 14.25.0
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 +30 -0
- package/README.md +15 -4
- package/SKILL.md +1 -1
- package/bin/install.mjs +15 -15
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/bootstrap.md +5 -1
- package/references/modes/mcp.md +9 -4
- package/references/modes/recommendations.md +17 -9
- package/references/modes/upgrade.md +3 -2
- package/tools/init-preflight.mjs +92 -0
- package/tools/init-project.mjs +400 -0
- package/tools/mcp.mjs +77 -19
- package/tools/recommendations.mjs +54 -62
- package/tools/write-lanes.mjs +171 -0
|
@@ -48,7 +48,8 @@ import {
|
|
|
48
48
|
SETTINGS_LOCAL_FILE,
|
|
49
49
|
} from './velocity-profile.mjs';
|
|
50
50
|
import { loadAutonomy, isSparseSeedConfig, AUTONOMY_REL } from './autonomy-config.mjs';
|
|
51
|
-
import { deriveDoctorPlan } from './autonomy-doctor.mjs';
|
|
51
|
+
import { deriveDoctorPlan, TRUSTED_DIRS } from './autonomy-doctor.mjs';
|
|
52
|
+
import { laneOf, routeLine } from './write-lanes.mjs';
|
|
52
53
|
import { detectBackends, findOnPath } from './detect-backends.mjs';
|
|
53
54
|
import { isDirectRun } from './direct-run.mjs';
|
|
54
55
|
import { ACTIVITIES, resolveActivityRecipe, composeReadiness, safeLine } from './recipes.mjs';
|
|
@@ -60,7 +61,6 @@ import { loadDeclaration, canonicalCheckerGates, coverageProducerPrecedes, isKit
|
|
|
60
61
|
import { readRegularFileNoFollow } from './fs-read-nofollow.mjs';
|
|
61
62
|
// The typed channel's READ-ONLY leaf only — never tools/mcp.mjs, which reaches the write core.
|
|
62
63
|
import {
|
|
63
|
-
MCP_JSON_REL,
|
|
64
64
|
SERVER_NAME as MCP_SERVER_NAME,
|
|
65
65
|
STATE as MCP_STATE,
|
|
66
66
|
readRegistration,
|
|
@@ -103,7 +103,7 @@ import {
|
|
|
103
103
|
import { ADOPTION, STORE_DIR_REL as SPEC_STORE_DIR_REL, SPEC_ADOPTION_LANE, declineFingerprint, readDeclineAck, surveySpecAdoption } from './spec-adoption.mjs';
|
|
104
104
|
import { ENSURE_OPS } from './ensure-vocabulary.mjs';
|
|
105
105
|
import { composeProfileGapScreen } from './profile-gap-screen.mjs';
|
|
106
|
-
import { NO_APPLY_LINE, SKILL_PINS, claudeDirOf,
|
|
106
|
+
import { NO_APPLY_LINE, SKILL_PINS, claudeDirOf, keySet, placeOf, printable, skillLine, skillState, skillTargets } from './jev-facts.mjs';
|
|
107
107
|
|
|
108
108
|
// The upgrade ensure that seeds the spec store — the not-adopted item's apply; pinned to the vocabulary.
|
|
109
109
|
const SPEC_LAYER_ENSURE = ENSURE_OPS.includes('specs') ? 'specs' : null;
|
|
@@ -117,7 +117,7 @@ export { ACKS_FILE, ACKS_LANE_KEY, ACKS_WORKTREES_DIR_KEY, ACKS_COVERAGE_DOMAIN_
|
|
|
117
117
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
118
118
|
const toolPath = (rel) => join(HERE, rel);
|
|
119
119
|
const q = shellQuoteArg;
|
|
120
|
-
const
|
|
120
|
+
const ABSENT_CODE = 'ENOENT';
|
|
121
121
|
const ENFORCEMENT_KIT_ROOT = resolve(HERE, '..');
|
|
122
122
|
const ENFORCEMENT_BUNDLE_ROOT = join(ENFORCEMENT_KIT_ROOT, 'references', 'scripts');
|
|
123
123
|
const ENFORCEMENT_WHAT = 'the enforcement scripts are not deployed beside docs/ai (no Node evidence) \u2014 the offer copies them and installs the pre-commit hook';
|
|
@@ -155,6 +155,7 @@ export const SEVERITIES = Object.freeze({
|
|
|
155
155
|
'autonomy-policy': SEVERITY_OPTIONAL,
|
|
156
156
|
'autonomy-render': SEVERITY_ATTENTION,
|
|
157
157
|
'sandbox-provision': SEVERITY_OPTIONAL,
|
|
158
|
+
'sandbox-provision.installable': SEVERITY_OPTIONAL,
|
|
158
159
|
'review-recipe': SEVERITY_ATTENTION,
|
|
159
160
|
'gates-declaration': SEVERITY_OPTIONAL,
|
|
160
161
|
'gates-inert': SEVERITY_ATTENTION,
|
|
@@ -185,12 +186,7 @@ export const SEVERITIES = Object.freeze({
|
|
|
185
186
|
'read-lane.stale': SEVERITY_ATTENTION,
|
|
186
187
|
'read-lane.missing': SEVERITY_ATTENTION,
|
|
187
188
|
'state-block': SEVERITY_OPTIONAL,
|
|
188
|
-
// The typed channel. The base arm is an ordinary offer; `.masked` stays an offer too (nothing is
|
|
189
|
-
// broken — the kit simply cannot write through a device node, so the remedy is handed over);
|
|
190
|
-
// `.differing` reports a CONFIGURED declaration that would launch something else, which is the
|
|
191
|
-
// one state here a maintainer must actually look at.
|
|
192
189
|
'mcp-channel': SEVERITY_OPTIONAL,
|
|
193
|
-
'mcp-channel.masked': SEVERITY_OPTIONAL,
|
|
194
190
|
'mcp-channel.differing': SEVERITY_ATTENTION,
|
|
195
191
|
agents: SEVERITY_OPTIONAL,
|
|
196
192
|
// The `agents` row above is an OFFER to place vehicles. This one reports a config that ALREADY
|
|
@@ -276,7 +272,6 @@ export const WHATS = Object.freeze({
|
|
|
276
272
|
'read-lane.missing': 'the gate hook is wired but its placed file is missing — every Bash call errors and the read-lane is dark; re-place it',
|
|
277
273
|
'state-block': 'nothing checks the closing state block — a turn that ends on «nothing needed from you», or on a promise it never started, passes unseen',
|
|
278
274
|
'mcp-channel': "the kit's read-only MCP server is not registered here — path questions and literal searches stay shell strings",
|
|
279
|
-
'mcp-channel.masked': '{rel} is a {className} here (a sandbox device mask is the usual cause), so the entry to merge is printed instead',
|
|
280
275
|
'mcp-channel.differing': 'an "{server}" MCP entry is already declared here and DIFFERS from the registration this kit copy would write',
|
|
281
276
|
agents: '{n} bundled subagent vehicle(s) not placed (Claude Code) — {ro} read-only, {ex} the full-tool executor; the apply PREVIEWS first',
|
|
282
277
|
'executor-vehicle': '{n} slot(s) configured subagent but the executor vehicle is {state}{reason} — every such slot runs solo until it is usable',
|
|
@@ -507,12 +502,18 @@ const probeSandboxProvision = ({ root, deps, add, skip }) => {
|
|
|
507
502
|
try {
|
|
508
503
|
const p = probeSandboxAvailability(deps);
|
|
509
504
|
if (p.available) return;
|
|
510
|
-
const
|
|
511
|
-
const
|
|
512
|
-
const
|
|
505
|
+
const isExec = deps.isExecutable ?? isExecutableFile;
|
|
506
|
+
const plan = deriveDoctorPlan({ probeResult: p, env: deps.env ?? process.env, isExec });
|
|
507
|
+
const installable = plan.tuple !== null && plan.untrusted.length === 0 && !plan.envMissing
|
|
508
|
+
&& ((deps.euid ?? (() => process.geteuid?.()))() === 0 || TRUSTED_DIRS.some((dir) => isExec(join(dir, 'sudo'))));
|
|
509
|
+
const variant = installable ? 'sandbox-provision.installable' : 'sandbox-provision';
|
|
510
|
+
const reason = truncatedTo(oneLineOf(p.reason), templateBudget(WHATS[variant]) - (installable ? plan.tuple.length : 0));
|
|
513
511
|
// The doctor reads process.cwd() (deployment-gated) and takes no --cwd flag — the one-liner
|
|
514
512
|
// pins the target project via a cd prefix (Segment B).
|
|
515
|
-
add('sandbox-provision', fillTemplate(WHATS[variant], { reason, tuple: plan.tuple }),
|
|
513
|
+
add('sandbox-provision', fillTemplate(WHATS[variant], { reason, tuple: plan.tuple }),
|
|
514
|
+
`cd ${q(root)} && node ${q(toolPath('autonomy-doctor.mjs'))}`, variant,
|
|
515
|
+
installable ? null : 'on native Windows Claude Code runs no sandbox: this item takes no effect there',
|
|
516
|
+
);
|
|
516
517
|
} catch (err) {
|
|
517
518
|
skip('sandbox-provision', err);
|
|
518
519
|
}
|
|
@@ -884,6 +885,7 @@ const probeCommitGuard = ({ root, deps, add, skip }) => {
|
|
|
884
885
|
if (hooksPath === null) return; // not a git tree — nothing to hook
|
|
885
886
|
const hook = (() => {
|
|
886
887
|
try {
|
|
888
|
+
if (readsAbsent(deps, join(hooksPath, 'pre-commit'))) return null;
|
|
887
889
|
return String(read(join(hooksPath, 'pre-commit'), 'utf8'));
|
|
888
890
|
} catch {
|
|
889
891
|
return null;
|
|
@@ -904,7 +906,7 @@ const readEnforcementNode = (path, lstat) => {
|
|
|
904
906
|
try {
|
|
905
907
|
return lstat(path);
|
|
906
908
|
} catch (err) {
|
|
907
|
-
if (err?.code ===
|
|
909
|
+
if (err?.code === ABSENT_CODE) return null;
|
|
908
910
|
throw new Error(`${path}: ${err?.code ?? err?.message ?? err}`);
|
|
909
911
|
}
|
|
910
912
|
};
|
|
@@ -954,7 +956,7 @@ const probeReadLane = ({ root, deps, add, skip, shared }) => {
|
|
|
954
956
|
// is silently dark; surface it as attention with a place-first recovery (council R2-M2).
|
|
955
957
|
const placeRecovery = () =>
|
|
956
958
|
add('read-lane', fillTemplate(WHATS['read-lane.missing'], {}), `node ${q(toolPath('gate-hook.mjs'))} --apply --cwd ${q(root)}`, 'read-lane.missing');
|
|
957
|
-
if (!sg.filePlaced) {
|
|
959
|
+
if (!sg.filePlaced || readsAbsent(deps, join(root, GATE_HOOK_REL))) {
|
|
958
960
|
placeRecovery();
|
|
959
961
|
return;
|
|
960
962
|
}
|
|
@@ -1099,9 +1101,9 @@ const probeExecutorVehicle = ({ root, deps, add, skip }) => {
|
|
|
1099
1101
|
}),
|
|
1100
1102
|
// The writer places a missing vehicle and re-derives a placed executor whose bytes differ from the derived body (an invalid
|
|
1101
1103
|
// frontmatter or read-only tools included), so those states need no precondition; every precondition above is a refusal the apply would meet.
|
|
1102
|
-
|
|
1104
|
+
`node ${q(toolPath('cheap-agents.mjs'))} --apply --cwd ${q(root)}`,
|
|
1103
1105
|
'executor-vehicle',
|
|
1104
|
-
`hidden-mode deployments only: after the apply, run node ${q(toolPath('hide-footprint.mjs'))} --dir ${q(root)} --reconcile so the placed .claude/agents/ stays invisible to git status`,
|
|
1106
|
+
`${preconditions.length ? `precondition: ${preconditions.join('; ')}; ` : ''}hidden-mode deployments only: after the apply, run node ${q(toolPath('hide-footprint.mjs'))} --dir ${q(root)} --reconcile so the placed .claude/agents/ stays invisible to git status`,
|
|
1105
1107
|
);
|
|
1106
1108
|
} catch (err) {
|
|
1107
1109
|
skip('executor-vehicle', err);
|
|
@@ -1234,7 +1236,17 @@ const isStateBlockGuardWired = (data) => {
|
|
|
1234
1236
|
// itself (a hook that is not there belongs to the place offers, never to a refresh arm); a symlink,
|
|
1235
1237
|
// a directory and an unreadable target all THROW into the probe's stated-skip lane.
|
|
1236
1238
|
const HOOK_CURRENCY = Object.freeze({ CURRENT: 'current', STALE: 'stale', ABSENT: 'absent' });
|
|
1239
|
+
const readsAbsent = (deps, path) => {
|
|
1240
|
+
try {
|
|
1241
|
+
(deps.lstat ?? lstatSync)(path);
|
|
1242
|
+
return false;
|
|
1243
|
+
} catch (err) {
|
|
1244
|
+
return err?.code === ABSENT_CODE;
|
|
1245
|
+
}
|
|
1246
|
+
};
|
|
1247
|
+
|
|
1237
1248
|
const readPlacedHookCurrency = (root, deps) => {
|
|
1249
|
+
if (readsAbsent(deps, join(root, GATE_HOOK_REL))) return HOOK_CURRENCY.ABSENT;
|
|
1238
1250
|
const read = deps.readRegularFileNoFollow ?? readRegularFileNoFollow;
|
|
1239
1251
|
const placed = read(join(root, GATE_HOOK_REL));
|
|
1240
1252
|
if (placed.outcome === 'absent') return HOOK_CURRENCY.ABSENT;
|
|
@@ -1521,9 +1533,6 @@ export const probeAdrStore = ({ root, deps, add, skip }) => {
|
|
|
1521
1533
|
|
|
1522
1534
|
// The typed-channel item. It asks the READ-ONLY registration leaf and nothing else — importing the
|
|
1523
1535
|
// writer would pull the atomic-write core into the advisor's graph (read-graph-purity.test.mjs).
|
|
1524
|
-
// The rendered apply is the mode's FLAGLESS preview on purpose: registering an MCP server means the
|
|
1525
|
-
// client will RUN that command, so the entry is read before it is declared and the `--apply` stays
|
|
1526
|
-
// the maintainer's separate step.
|
|
1527
1536
|
const probeMcpChannel = ({ root, deps, add, skip }) => {
|
|
1528
1537
|
try {
|
|
1529
1538
|
const registration = readRegistration(root, deps);
|
|
@@ -1538,49 +1547,16 @@ const probeMcpChannel = ({ root, deps, add, skip }) => {
|
|
|
1538
1547
|
// settings half can end the probe. A differing entry is fully observable, and an unreadable
|
|
1539
1548
|
// settings file says nothing about it — returning on the settings mask first hid it.
|
|
1540
1549
|
assertReadable(registration.mcpJson);
|
|
1541
|
-
const settingsUsable = registration.settings.state !== MCP_STATE.MASKED
|
|
1542
|
-
&& registration.settings.state !== MCP_STATE.FOREIGN
|
|
1543
|
-
&& registration.settings.state !== MCP_STATE.UNREADABLE
|
|
1544
|
-
&& registration.settings.state !== MCP_STATE.MALFORMED;
|
|
1545
1550
|
if (registration.mcpJson.differs) {
|
|
1546
|
-
// The remedy is the maintainer's edit either way; the "then run" tail is dropped where that
|
|
1547
|
-
// command could not succeed, so the item never hands over a line that exits 1.
|
|
1548
|
-
const tail = settingsUsable ? `, then run ${preview}` : '';
|
|
1549
1551
|
add(
|
|
1550
1552
|
'mcp-channel',
|
|
1551
1553
|
fillTemplate(WHATS['mcp-channel.differing'], { server: MCP_SERVER_NAME }),
|
|
1552
|
-
|
|
1554
|
+
preview,
|
|
1553
1555
|
'mcp-channel.differing',
|
|
1554
1556
|
);
|
|
1555
1557
|
return;
|
|
1556
1558
|
}
|
|
1557
1559
|
assertReadable(registration.settings);
|
|
1558
|
-
// The HAND-APPLY arm renders the MODE's own preview, so it may fire only where that command can
|
|
1559
|
-
// actually run — and the writer refuses a masked settings.json outright (it can neither write it
|
|
1560
|
-
// nor merge into what it cannot read). A masked settings half is therefore a stated SKIP: the
|
|
1561
|
-
// completeness this item decides on is unknowable, and offering a command that exits 1 is worse
|
|
1562
|
-
// than saying nothing.
|
|
1563
|
-
if (registration.settings.state === MCP_STATE.MASKED) {
|
|
1564
|
-
skip('mcp-channel', new Error(`${registration.settings.rel} is a ${registration.settings.className} here (a sandbox device mask is the usual cause) — the registration cannot be judged or written from in here; verify it outside the sandbox`));
|
|
1565
|
-
return;
|
|
1566
|
-
}
|
|
1567
|
-
if (registration.mcpJson.state === MCP_STATE.MASKED) {
|
|
1568
|
-
// The kit cannot write through the mask either, so the remedy is HAND-APPLY. But when the
|
|
1569
|
-
// settings half is already complete, the registration was almost certainly made from outside
|
|
1570
|
-
// the sandbox: what cannot be observed becomes a stated SKIP (optimality withheld), never the
|
|
1571
|
-
// same offer again on every single upgrade.
|
|
1572
|
-
if (registration.settings.complete) {
|
|
1573
|
-
skip('mcp-channel', new Error(`${registration.mcpJson.rel} is a ${registration.mcpJson.className} here (a sandbox device mask is the usual cause) and the settings half is already complete — verify the entry outside the sandbox`));
|
|
1574
|
-
return;
|
|
1575
|
-
}
|
|
1576
|
-
add(
|
|
1577
|
-
'mcp-channel',
|
|
1578
|
-
fillTemplate(WHATS['mcp-channel.masked'], { rel: registration.mcpJson.rel, className: registration.mcpJson.className }),
|
|
1579
|
-
`HAND-APPLY: ${preview}`,
|
|
1580
|
-
'mcp-channel.masked',
|
|
1581
|
-
);
|
|
1582
|
-
return;
|
|
1583
|
-
}
|
|
1584
1560
|
if (registration.registered) return;
|
|
1585
1561
|
add('mcp-channel', fillTemplate(WHATS['mcp-channel'], {}), preview);
|
|
1586
1562
|
} catch (err) {
|
|
@@ -1589,16 +1565,16 @@ const probeMcpChannel = ({ root, deps, add, skip }) => {
|
|
|
1589
1565
|
};
|
|
1590
1566
|
|
|
1591
1567
|
// The Jev offer (contract: kit/jev-guide, part jev-offer). The key is judged first, through the facts leaf's one rule over
|
|
1592
|
-
// the injected environment
|
|
1568
|
+
// the injected environment.
|
|
1593
1569
|
export const probeJevConnect = ({ root, deps, add, skip }) => {
|
|
1594
1570
|
try {
|
|
1595
1571
|
const env = deps.getenv ?? {};
|
|
1596
|
-
if (keySet(env)) return;
|
|
1572
|
+
if (deps.keySet === true || keySet(env)) return;
|
|
1597
1573
|
if (readAckValue(root, deps, ACKS_JEV_CONNECT_KEY) === JEV_CONNECT_DECLINE) return;
|
|
1598
1574
|
const host = deps.jevHost ?? {};
|
|
1599
1575
|
const place = placeOf({ env, platform: host.platform, hostname: host.hostname, container: host.container });
|
|
1600
1576
|
const decline = `node ${q(toolPath('ack-write.mjs'))} --lane jev-connect --fingerprint ${JEV_CONNECT_DECLINE} --cwd ${q(root)}`;
|
|
1601
|
-
add('jev-connect', WHATS['jev-connect'],
|
|
1577
|
+
add('jev-connect', WHATS['jev-connect'], routeLine('jev-connect'), 'jev-connect',
|
|
1602
1578
|
`${place ? `run the apply in ${place}; ` : ''}HAND-APPLY alternative (instead of the apply, never after it): decline the offer by recording it — ${decline}`);
|
|
1603
1579
|
} catch (err) {
|
|
1604
1580
|
skip('jev-connect', err);
|
|
@@ -1610,7 +1586,7 @@ export const probeJevConnect = ({ root, deps, add, skip }) => {
|
|
|
1610
1586
|
export const probeJevSkill = ({ root, deps, add, skip }) => {
|
|
1611
1587
|
try {
|
|
1612
1588
|
const env = deps.getenv ?? {};
|
|
1613
|
-
if (!keySet(env)) return;
|
|
1589
|
+
if (deps.keySet !== true && !keySet(env)) return;
|
|
1614
1590
|
const host = deps.jevHost ?? {};
|
|
1615
1591
|
if (typeof host.home !== 'string' || host.home === '') return;
|
|
1616
1592
|
const pins = deps.jevPins ?? SKILL_PINS;
|
|
@@ -1710,14 +1686,29 @@ export const buildRecommendations = ({ cwd, deps = {} } = {}) => {
|
|
|
1710
1686
|
skip(key, new Error(`item shape violation — ${problems.join('; ')}`));
|
|
1711
1687
|
return false;
|
|
1712
1688
|
}
|
|
1713
|
-
items.push({ key, variant, severity: SEVERITIES[variant], what, benefit: BENEFITS[key], apply, detail });
|
|
1689
|
+
items.push({ key, variant, severity: SEVERITIES[variant], lane: laneOf(variant), what, benefit: BENEFITS[key], apply: routeLine(variant) ?? apply, detail });
|
|
1714
1690
|
return true;
|
|
1715
1691
|
};
|
|
1716
1692
|
// The per-run scratch a probe uses to tell a LATER probe what it actually did. Written by exactly
|
|
1717
1693
|
// one pair today (the marker-stale ⟷ read-lane.stale precedence) and read in the frozen PROBES
|
|
1718
1694
|
// order, so the reader can never run first.
|
|
1719
1695
|
const shared = {};
|
|
1720
|
-
|
|
1696
|
+
const contentDeps = Object.create(Object.getPrototypeOf(deps), {
|
|
1697
|
+
...Object.getOwnPropertyDescriptors(deps),
|
|
1698
|
+
lstat: {
|
|
1699
|
+
enumerable: true,
|
|
1700
|
+
value: (path, ...args) => {
|
|
1701
|
+
const stat = (deps.lstat ?? deps.lstatSync ?? lstatSync)(path, ...args);
|
|
1702
|
+
if (stat.isCharacterDevice?.() || stat.isBlockDevice?.()) {
|
|
1703
|
+
throw Object.assign(new Error(`${ABSENT_CODE}: ${path}`), { code: ABSENT_CODE });
|
|
1704
|
+
}
|
|
1705
|
+
return stat;
|
|
1706
|
+
},
|
|
1707
|
+
},
|
|
1708
|
+
});
|
|
1709
|
+
for (const probe of deps.probes ?? PROBES) {
|
|
1710
|
+
probe({ root, deps: probe === probeMasksItem ? deps : contentDeps, add, skip, shared });
|
|
1711
|
+
}
|
|
1721
1712
|
return { root, items, skips };
|
|
1722
1713
|
};
|
|
1723
1714
|
|
|
@@ -1743,6 +1734,7 @@ export const formatRecommendations = ({ items, skips }) => {
|
|
|
1743
1734
|
ordered.forEach((item, i) => {
|
|
1744
1735
|
lines.push(`${i + 1}. ${SEVERITY_LABELS[item.severity] ?? SEVERITY_LABELS[SEVERITY_OPTIONAL]}: ${item.what}`);
|
|
1745
1736
|
lines.push(` benefit: ${item.benefit}`);
|
|
1737
|
+
lines.push(` lane: ${item.lane}`);
|
|
1746
1738
|
if (item.detail) lines.push(` recipe: ${item.detail}`);
|
|
1747
1739
|
lines.push(` apply: ${item.apply}`);
|
|
1748
1740
|
});
|
|
@@ -1776,7 +1768,7 @@ Read-only: never writes, never commits, never runs a subscription CLI. Exit code
|
|
|
1776
1768
|
rendered (items or empty); 1 error; 2 usage.`;
|
|
1777
1769
|
|
|
1778
1770
|
// The host facts of the key and skill offers: the platform, the hostname (a throw reads as none), /.dockerenv a regular file, the home.
|
|
1779
|
-
const hostFacts = () => ({ platform: process.platform, hostname: (() => { try { return hostname(); } catch { return ''; } })(),
|
|
1771
|
+
export const hostFacts = () => ({ platform: process.platform, hostname: (() => { try { return hostname(); } catch { return ''; } })(),
|
|
1780
1772
|
container: (() => { try { return statSync('/.dockerenv').isFile(); } catch { return false; } })(),
|
|
1781
1773
|
home: (() => { try { return homedir(); } catch { return ''; } })() });
|
|
1782
1774
|
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { join, resolve } from 'node:path';
|
|
2
|
+
import { dirCovers } from './declared-paths.mjs';
|
|
3
|
+
import { claudeDirOf, skillTargets } from './jev-facts.mjs';
|
|
4
|
+
import { targetsFor } from './jev-connect.mjs';
|
|
5
|
+
|
|
6
|
+
export const LANE_CONSOLE = 'console';
|
|
7
|
+
export const LANE_CHAT = 'chat';
|
|
8
|
+
|
|
9
|
+
export const VARIANT_LANES = Object.freeze({
|
|
10
|
+
'velocity-core': LANE_CONSOLE,
|
|
11
|
+
'kit-tools-tier': LANE_CONSOLE,
|
|
12
|
+
'bridge-tier': LANE_CONSOLE,
|
|
13
|
+
'autonomy-policy': LANE_CHAT,
|
|
14
|
+
'autonomy-render': LANE_CONSOLE,
|
|
15
|
+
'sandbox-provision': LANE_CONSOLE,
|
|
16
|
+
'sandbox-provision.installable': LANE_CONSOLE,
|
|
17
|
+
'review-recipe': LANE_CHAT,
|
|
18
|
+
'gates-declaration': LANE_CHAT,
|
|
19
|
+
'gates-inert': LANE_CHAT,
|
|
20
|
+
'gates-inert.no-verification': LANE_CHAT,
|
|
21
|
+
'gates-inert.producer-unrecognized': LANE_CHAT,
|
|
22
|
+
'gates-inert.coverage-domain-narrow': LANE_CHAT,
|
|
23
|
+
'source-size': LANE_CHAT,
|
|
24
|
+
'source-size.unminted': LANE_CHAT,
|
|
25
|
+
'source-size.adopted-elsewhere': LANE_CHAT,
|
|
26
|
+
'source-size.id-squatter': LANE_CHAT,
|
|
27
|
+
'gate-hook': LANE_CONSOLE,
|
|
28
|
+
'gate-hook.marker-stale': LANE_CONSOLE,
|
|
29
|
+
'commit-guard': LANE_CONSOLE,
|
|
30
|
+
enforcement: LANE_CONSOLE,
|
|
31
|
+
'read-lane': LANE_CHAT,
|
|
32
|
+
'read-lane.stale': LANE_CONSOLE,
|
|
33
|
+
'read-lane.missing': LANE_CONSOLE,
|
|
34
|
+
'state-block': LANE_CONSOLE,
|
|
35
|
+
'mcp-channel': LANE_CONSOLE,
|
|
36
|
+
'mcp-channel.differing': LANE_CONSOLE,
|
|
37
|
+
agents: LANE_CONSOLE,
|
|
38
|
+
'executor-vehicle': LANE_CONSOLE,
|
|
39
|
+
'family-freshness': LANE_CONSOLE,
|
|
40
|
+
'adr-store-migration': LANE_CHAT,
|
|
41
|
+
'sandbox-masks': LANE_CHAT,
|
|
42
|
+
'sandbox-masks.unfenced-mount': LANE_CHAT,
|
|
43
|
+
'sandbox-lane': LANE_CHAT,
|
|
44
|
+
'worktrees-dir': LANE_CHAT,
|
|
45
|
+
'spec-adoption': LANE_CHAT,
|
|
46
|
+
'profile-gap': LANE_CHAT,
|
|
47
|
+
'jev-connect': LANE_CONSOLE,
|
|
48
|
+
'jev-skill': LANE_CONSOLE,
|
|
49
|
+
'jev-skill.earlier': LANE_CONSOLE,
|
|
50
|
+
'spec-adoption.adopting': LANE_CHAT,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export const laneOf = (variant) => {
|
|
54
|
+
if (!Object.hasOwn(VARIANT_LANES, variant)) {
|
|
55
|
+
throw new Error(`unregistered variant: ${variant}`);
|
|
56
|
+
}
|
|
57
|
+
return VARIANT_LANES[variant];
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const SETTINGS_PATH = join('.claude', 'settings.json');
|
|
61
|
+
const PROTECTED_FILES = [
|
|
62
|
+
SETTINGS_PATH,
|
|
63
|
+
join('.claude', 'settings.local.json'),
|
|
64
|
+
'.mcp.json',
|
|
65
|
+
join('.git', 'config'),
|
|
66
|
+
];
|
|
67
|
+
const PROTECTED_DIRS = ['agents', 'hooks', 'skills'].map((name) => join('.claude', name));
|
|
68
|
+
|
|
69
|
+
export const isProtectedPath = (abs, { root, home, hooksDir }) => {
|
|
70
|
+
const project = resolve(root);
|
|
71
|
+
const hooks = resolve(hooksDir);
|
|
72
|
+
const path = resolve(abs);
|
|
73
|
+
if (!dirCovers(project, hooks)) {
|
|
74
|
+
throw new Error(`hooksDir outside the project: ${hooksDir}`);
|
|
75
|
+
}
|
|
76
|
+
if (!dirCovers(project, path)) return dirCovers(resolve(home), path);
|
|
77
|
+
return PROTECTED_FILES.some((file) => path === join(project, file))
|
|
78
|
+
|| [...PROTECTED_DIRS.map((dir) => join(project, dir)), hooks].some((dir) => dirCovers(dir, path));
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const ROUTE = "applies at your next npx @sabaiway/agent-workflow-kit@latest init, run from this project's folder";
|
|
82
|
+
const RESTART = '; if init reports it not pending, restart the agent from that console';
|
|
83
|
+
const SETTINGS_WRITES = [SETTINGS_PATH];
|
|
84
|
+
const HOOK_WRITES = [join('.claude', 'hooks', 'agent-workflow-gates.mjs'), ...SETTINGS_WRITES];
|
|
85
|
+
const MCP_WRITES = ['.mcp.json', ...SETTINGS_WRITES];
|
|
86
|
+
const AGENT_WRITES = [join('.claude', 'agents')];
|
|
87
|
+
|
|
88
|
+
const getSkillPaths = ({ env, home, platform }) => {
|
|
89
|
+
const claudeDir = claudeDirOf(env, home, platform).dir;
|
|
90
|
+
return skillTargets({ home, claudeDir }).map(({ path }) => path);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const getStartupPaths = ({ env, home, platform }) => targetsFor({
|
|
94
|
+
shell: env.SHELL,
|
|
95
|
+
home,
|
|
96
|
+
zdotdir: env.ZDOTDIR,
|
|
97
|
+
xdgConfigHome: env.XDG_CONFIG_HOME,
|
|
98
|
+
platform,
|
|
99
|
+
}).filter(({ path }) => path !== null).map(({ path }) => path);
|
|
100
|
+
|
|
101
|
+
const createForm = (file, preview, apply, writes, restart = false) => Object.freeze({
|
|
102
|
+
file,
|
|
103
|
+
preview,
|
|
104
|
+
apply,
|
|
105
|
+
writes,
|
|
106
|
+
route: restart ? ROUTE + RESTART : ROUTE,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const CORE_FORM = createForm('velocity-profile.mjs', ['--cwd', '<root>'], ['--apply', '--cwd', '<root>'], SETTINGS_WRITES);
|
|
110
|
+
const HOOK_FORM = createForm('gate-hook.mjs', ['--cwd', '<root>'], ['--apply', '--cwd', '<root>'], HOOK_WRITES);
|
|
111
|
+
const AGENT_FORM = createForm('cheap-agents.mjs', ['--cwd', '<root>'], ['--apply', '--cwd', '<root>'], AGENT_WRITES);
|
|
112
|
+
const SKILL_FORM = createForm('jev-skill.mjs', ['--claude-dir', '<dir>'], ['--apply', '--claude-dir', '<dir>'], getSkillPaths, true);
|
|
113
|
+
|
|
114
|
+
const FORMS = Object.freeze({
|
|
115
|
+
'velocity-core': CORE_FORM,
|
|
116
|
+
'kit-tools-tier': createForm('velocity-profile.mjs',
|
|
117
|
+
['--kit-tools', '--cwd', '<root>'], ['--apply', '--kit-tools', '--cwd', '<root>'], SETTINGS_WRITES),
|
|
118
|
+
'bridge-tier': createForm('velocity-profile.mjs',
|
|
119
|
+
['--bridge-tier', '--cwd', '<root>'], ['--apply', '--bridge-tier', '--cwd', '<root>'], SETTINGS_WRITES, true),
|
|
120
|
+
'autonomy-render': createForm('velocity-profile.mjs',
|
|
121
|
+
['--autonomy', '--cwd', '<root>'], ['--autonomy', '--apply', '--cwd', '<root>'], SETTINGS_WRITES),
|
|
122
|
+
'gate-hook': HOOK_FORM,
|
|
123
|
+
'read-lane.missing': HOOK_FORM,
|
|
124
|
+
'mcp-channel': createForm('mcp.mjs', ['--cwd', '<root>'], ['--apply', '--cwd', '<root>'], MCP_WRITES),
|
|
125
|
+
'mcp-channel.differing': createForm('mcp.mjs',
|
|
126
|
+
['--replace', '--cwd', '<root>'], ['--replace', '--apply', '--cwd', '<root>'], MCP_WRITES),
|
|
127
|
+
agents: AGENT_FORM,
|
|
128
|
+
'executor-vehicle': AGENT_FORM,
|
|
129
|
+
'sandbox-provision.installable': createForm('autonomy-doctor.mjs', [], ['--apply', '<tuple>'], [], true),
|
|
130
|
+
'jev-skill': SKILL_FORM,
|
|
131
|
+
'jev-skill.earlier': SKILL_FORM,
|
|
132
|
+
'jev-connect': createForm('jev-connect.mjs', null, [], getStartupPaths, true),
|
|
133
|
+
'family-freshness': { route: ROUTE },
|
|
134
|
+
'sandbox-provision': null,
|
|
135
|
+
'commit-guard': null,
|
|
136
|
+
'gate-hook.marker-stale': null,
|
|
137
|
+
'read-lane.stale': null,
|
|
138
|
+
'state-block': null,
|
|
139
|
+
enforcement: null,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const resolveArgument = (arg, facts) => {
|
|
143
|
+
if (arg === '<root>') return facts.root;
|
|
144
|
+
if (arg === '<tuple>') return facts.tuple;
|
|
145
|
+
if (arg === '<dir>') return claudeDirOf(facts.env, facts.home, facts.platform).dir;
|
|
146
|
+
return arg;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export const consoleArgv = (variant, facts) => {
|
|
150
|
+
laneOf(variant);
|
|
151
|
+
const form = FORMS[variant];
|
|
152
|
+
if (!form?.file) return null;
|
|
153
|
+
const argv = (args) => ['node', join(facts.toolsDir, form.file), ...args.map((arg) => resolveArgument(arg, facts))];
|
|
154
|
+
return {
|
|
155
|
+
preview: form.preview === null ? null : argv(form.preview),
|
|
156
|
+
apply: argv(form.apply),
|
|
157
|
+
};
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
export const writesOf = (variant, facts) => {
|
|
161
|
+
laneOf(variant);
|
|
162
|
+
const form = FORMS[variant];
|
|
163
|
+
if (!form?.file) return null;
|
|
164
|
+
if (typeof form.writes === 'function') return form.writes(facts);
|
|
165
|
+
return form.writes.map((path) => join(facts.root, path));
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
export const routeLine = (variant) => {
|
|
169
|
+
laneOf(variant);
|
|
170
|
+
return FORMS[variant]?.route ?? null;
|
|
171
|
+
};
|