@north-light/crouter 0.3.274 → 0.3.276
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/builtin-memory/02-turn-lifecycle/01-terminal.md +1 -1
- package/dist/clients/attach/viewer.js +459 -459
- package/dist/commands/cron.js +3 -3
- package/dist/commands/node/create.js +3 -1
- package/dist/commands/node-worktree.js +7 -7
- package/dist/commands/pkg/plugin-manage.js +19 -8
- package/dist/commands/push.js +1 -1
- package/dist/core/__tests__/helpers/harness.d.ts +1 -0
- package/dist/core/__tests__/helpers/harness.js +12 -10
- package/dist/core/__tests__/integration/worktree-land.test.js +7 -2
- package/dist/core/__tests__/seam/broker-provider-retry.test.js +3 -16
- package/dist/core/canvas/browse/render.js +82 -67
- package/dist/core/canvas/status-glyph.d.ts +4 -3
- package/dist/core/canvas/status-glyph.js +17 -9
- package/dist/core/canvas/types.d.ts +18 -0
- package/dist/core/command-plugins/bundle.d.ts +7 -0
- package/dist/core/command-plugins/bundle.js +18 -4
- package/dist/core/git.d.ts +5 -1
- package/dist/core/git.js +18 -4
- package/dist/core/runtime/spawn.d.ts +2 -0
- package/dist/core/runtime/spawn.js +1 -1
- package/dist/core/runtime/surface-bg.d.ts +6 -4
- package/dist/core/runtime/surface-bg.js +39 -22
- package/dist/core/tui/draw.d.ts +2 -0
- package/dist/core/tui/draw.js +15 -0
- package/dist/core/worktree-sweep.js +28 -5
- package/dist/core/worktree.d.ts +1 -12
- package/dist/core/worktree.js +222 -10
- package/dist/daemon/api/handlers/crons.js +5 -4
- package/dist/daemon/api/handlers/messages.js +5 -0
- package/dist/daemon/api/handlers/nodes.js +2 -0
- package/dist/daemon/api/handlers/reports.js +11 -7
- package/dist/types.d.ts +1 -1
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
package/dist/commands/cron.js
CHANGED
|
@@ -134,7 +134,7 @@ const addLeaf = defineLeaf({
|
|
|
134
134
|
{ kind: 'flag', name: 'at', type: 'string', required: false, constraint: 'One-shot: run the stored bash once at <when> — a duration ("90s","1h30m"), a zoned ISO ("2026-06-07T09:00:00Z"), or a bare ISO ("2026-06-07T09:00", interpreted in --tz else host zone). Exactly one of --at / --every is required. The row is deleted after its run unless the run\'s disposition pauses it.' },
|
|
135
135
|
{ kind: 'flag', name: 'every', type: 'string', required: false, constraint: 'Recurring: run the stored bash at each cadence — a fixed interval ("6h" — first fire one interval from now), a 5-field cron ("0 9 * * *"), or an @alias ("@daily"). Each fire is another bash run. Minimum cadence 60s. Exactly one of --at / --every is required. For a gated cron (a command that exits 75 while ineligible), pick a LONG cadence: each natural slot is only the backstop re-check, and the daemon poke supplies the latency — "6h" plus poke beats "15m" polling on both latency and cost.' },
|
|
136
136
|
{ kind: 'flag', name: 'tz', type: 'string', required: false, constraint: 'IANA zone (e.g. "America/New_York") for a bare-ISO --at or a calendar --every. Defaults to the host zone.' },
|
|
137
|
-
{ kind: 'flag', name: 'on-output', type: 'enum', choices: ['silent', 'on-failure', 'always', 'on-change'], required: false,
|
|
137
|
+
{ kind: 'flag', name: 'on-output', type: 'enum', choices: ['silent', 'on-failure', 'always', 'on-change'], required: false, constraint: 'What each run does with its output. Defaults to on-change for a node:<id> sink; otherwise on-failure. silent: record in the run log, tell nobody — never escalate. on-failure: no success delivery; a failing run pauses the cron and spawns a node to deal with it. always: deliver stdout every run to --sink at --tier. on-change: deliver stdout only when it differs from the previous run\'s — the shape polling wants.' },
|
|
138
138
|
{ kind: 'flag', name: 'sink', type: 'string', required: false, constraint: 'Where always/on-change stdout deliveries go: "node:<id>" (an existing node\'s inbox — must exist now; gone/finalized at fire time is a failure), "spawn:<kind>" (each delivered output creates a self-finishing node with stdout as kickoff — a managed child while its creator exists, otherwise a terminal parentless root), or "human" (the humanloop inbox). Required by always/on-change, rejected otherwise.' },
|
|
139
139
|
{ kind: 'flag', name: 'tier', type: 'enum', choices: ['normal', 'urgent', 'critical'], required: false, default: 'urgent', constraint: 'Inbox priority of a node-sink delivery. urgent (default): steers the target mid-turn, or wakes it when dormant — a fire is news the node asked for on a schedule, so it should land now. normal: waits for the running turn to settle. critical: aborts the running turn outright.' },
|
|
140
140
|
{ kind: 'flag', name: 'expires', type: 'string', required: false, constraint: 'Clock bound (same grammar as --at): the row is deleted once this instant passes. The self-limiting half of a poll-until pattern. For a held one-shot (last run exited 75), expiry bounds the wait for eligibility — the row is deleted UNFIRED, never fired blind at expiry.' },
|
|
@@ -254,7 +254,7 @@ const addLeaf = defineLeaf({
|
|
|
254
254
|
env_json: envJson,
|
|
255
255
|
profile: profile ?? null,
|
|
256
256
|
scope,
|
|
257
|
-
on_output: input['onOutput']
|
|
257
|
+
on_output: input['onOutput'],
|
|
258
258
|
sink: typeof input['sink'] === 'string' && input['sink'] !== '' ? input['sink'] : null,
|
|
259
259
|
tier: input['tier'] ?? 'urgent',
|
|
260
260
|
expires_at: expiresAt,
|
|
@@ -525,7 +525,7 @@ export function registerCron() {
|
|
|
525
525
|
help: {
|
|
526
526
|
name: 'cron',
|
|
527
527
|
summary: 'scheduled bash commands, daemon-run',
|
|
528
|
-
model: 'A cron stores and daemon-runs arbitrary bash at its clock. Use ordinary bash for ordinary work; choose node:<id> for stdout as inbox information to an existing node, an existing node\'s lifecycle fresh-revive action in the bash for a clean re-check without inbox output, or spawn:<kind> to route each output-producing fire to a fresh self-finishing node (a managed child while its creator exists, otherwise a terminal parentless root). Per-fire fresh agent work belongs on the existing spawn:<kind> sink, not an independent resident-root birth. A gate is bash, at the top of the command. Exit 0 after deciding not to act and the occurrence is simply spent — right when the recurrence is your polling cadence. Exit 75 and the occurrence is OWED: the row is held, fires again the moment the daemon receives an eligibility poke from the host platform, and otherwise re-checks at its next natural slot (a held one-shot instead waits for a poke until --expires deletes it unfired). Any other nonzero exit is a real failure and escalates per --on-output. Use 75 for "not yet — retry when conditions change" (a device coming online); keep gate ERRORS nonzero so a broken probe is loud instead of silently parked. Beyond the exit-code contract, cron has no native predicate-trigger semantics. --at runs once and deletes the row unless the run\'s disposition pauses it; --every runs each cadence. Output disposition (--on-output) records silently, escalates failures, or routes stdout to its --sink. Anchor a cron to delete it with a node; --cancel-on-wake instead deletes it when the anchor wakes. An unanchored recurring cron ends through --expires, explicit cancellation, or self-cancellation from its bash. Every run lands in a bounded per-cron run log (`cron show`); cwd, env, and profile are snapshotted at arm time, and --scope controls who lists and cancels it.',
|
|
528
|
+
model: 'A cron stores and daemon-runs arbitrary bash at its clock. Use ordinary bash for ordinary work; choose node:<id> for stdout as inbox information to an existing node (its disposition defaults to on-change), an existing node\'s lifecycle fresh-revive action in the bash for a clean re-check without inbox output, or spawn:<kind> to route each output-producing fire to a fresh self-finishing node (a managed child while its creator exists, otherwise a terminal parentless root). Per-fire fresh agent work belongs on the existing spawn:<kind> sink, not an independent resident-root birth. A gate is bash, at the top of the command. Exit 0 after deciding not to act and the occurrence is simply spent — right when the recurrence is your polling cadence. Exit 75 and the occurrence is OWED: the row is held, fires again the moment the daemon receives an eligibility poke from the host platform, and otherwise re-checks at its next natural slot (a held one-shot instead waits for a poke until --expires deletes it unfired). Any other nonzero exit is a real failure and escalates per --on-output. Use 75 for "not yet — retry when conditions change" (a device coming online); keep gate ERRORS nonzero so a broken probe is loud instead of silently parked. Beyond the exit-code contract, cron has no native predicate-trigger semantics. --at runs once and deletes the row unless the run\'s disposition pauses it; --every runs each cadence. Output disposition (--on-output) records silently, escalates failures, or routes stdout to its --sink. Anchor a cron to delete it with a node; --cancel-on-wake instead deletes it when the anchor wakes. An unanchored recurring cron ends through --expires, explicit cancellation, or self-cancellation from its bash. Every run lands in a bounded per-cron run log (`cron show`); cwd, env, and profile are snapshotted at arm time, and --scope controls who lists and cancels it.',
|
|
529
529
|
},
|
|
530
530
|
children: [addLeaf, listLeaf, showLeaf, runLeaf, pauseLeaf, resumeLeaf, cancelLeaf],
|
|
531
531
|
});
|
|
@@ -12,6 +12,7 @@ import { stateBlock } from '../../core/help.js';
|
|
|
12
12
|
import { cliClient, getNodeOrNull, rethrowAsCliError } from '../api-client.js';
|
|
13
13
|
import { inTmux } from '../../core/runtime/placement-tmux.js';
|
|
14
14
|
import { openSpawnViewer } from '../surface/node/placement.js';
|
|
15
|
+
import { resolve } from 'node:path';
|
|
15
16
|
const YIELD_NUDGE_THRESHOLD = 100_000;
|
|
16
17
|
const NODE_NEW_OUTPUT_SCHEMA_EFFECT = '--output-schema writes the accepted result to the node’s context/result.json and pushes it as the node’s final report.';
|
|
17
18
|
const STD_CHILD_FOLLOW_UP = "Do not wait, poll, or duplicate this child's assignment in your own context — there is no result to await, stopping will not strand you, and duplicated work wastes the delegated context. You're auto-subscribed, so its finish wakes you on its own. Two moves only: do other independent work right now, or stop and end your turn — the wake brings you back.";
|
|
@@ -177,7 +178,8 @@ async function runNodeCreation(input) {
|
|
|
177
178
|
}
|
|
178
179
|
const kind = input['kind'];
|
|
179
180
|
const mode = (input['mode'] ?? 'base');
|
|
180
|
-
const
|
|
181
|
+
const pinCwdRaw = input['cwd']?.trim();
|
|
182
|
+
const pinCwd = pinCwdRaw !== undefined && pinCwdRaw !== '' ? resolve(pinCwdRaw) : undefined;
|
|
181
183
|
const name = input['name'];
|
|
182
184
|
const parent = input['parent'];
|
|
183
185
|
const root = input['root'] === true;
|
|
@@ -71,16 +71,16 @@ const worktreeAbandon = defineLeaf({
|
|
|
71
71
|
});
|
|
72
72
|
const worktreeClose = defineLeaf({
|
|
73
73
|
name: 'close',
|
|
74
|
-
description: 'land
|
|
75
|
-
whenToUse: 'you are inside the node that owns an open managed worktree and are ready to
|
|
74
|
+
description: 'land or safely close the current node-managed worktree',
|
|
75
|
+
whenToUse: 'you are inside the node that owns an open managed worktree and are ready to land its recorded branch into the local base, or to close a moved real branch whose already-delivered content can be safely preserved while its checkout is disposed later',
|
|
76
76
|
help: {
|
|
77
77
|
name: 'node worktree close',
|
|
78
|
-
summary: 'land
|
|
78
|
+
summary: 'land or safely close the current node-managed worktree',
|
|
79
79
|
params: [],
|
|
80
80
|
output: [
|
|
81
81
|
{ name: 'node_id', type: 'string', required: true, constraint: 'The caller node id whose managed worktree was closed.' },
|
|
82
|
-
{ name: 'branch', type: 'string', required: true, constraint: 'The
|
|
83
|
-
{ name: 'landed_sha', type: 'string', required: true, constraint: 'The
|
|
82
|
+
{ name: 'branch', type: 'string', required: true, constraint: 'The branch whose delivered content is retained: fast-forwarded into the recorded local base in an ordinary close, or preserved unchanged when a moved real branch is authorized for disposal.' },
|
|
83
|
+
{ name: 'landed_sha', type: 'string', required: true, constraint: 'The retained branch tip: fast-forwarded into the local base in an ordinary close, or proven already contained in the local base or its upstream before a moved real branch is authorized for disposal.' },
|
|
84
84
|
{ name: 'worktree_path', type: 'string', required: true, constraint: 'The managed worktree path whose cleanup is deferred until its caller exits.' },
|
|
85
85
|
{ name: 'worktree_removed', type: 'boolean', required: true, constraint: 'Always false for an active caller: its checkout remains intact rather than being removed beneath the node.' },
|
|
86
86
|
{ name: 'worktree_remove_error', type: 'string', required: true, constraint: 'The named deferred-cleanup state and safe manual follow-up after the caller exits.' },
|
|
@@ -89,7 +89,7 @@ const worktreeClose = defineLeaf({
|
|
|
89
89
|
],
|
|
90
90
|
outputKind: 'object',
|
|
91
91
|
effects: [
|
|
92
|
-
'Rebases the worktree onto its recorded local base branch
|
|
92
|
+
'Rebases the worktree onto its recorded local base branch and fast-forwards that base onto it (never pushes to origin), marking the node-managed worktree closed with cleanup pending and leaving the active caller checkout and branch intact. When the recorded branch is gone because ordinary work moved this checkout onto a real branch, and that branch\'s content is already contained in the recorded base, closes by authorizing the checkout\'s later removal instead of landing — preserving that branch rather than deleting it.',
|
|
93
93
|
],
|
|
94
94
|
},
|
|
95
95
|
run: async () => {
|
|
@@ -105,7 +105,7 @@ const worktreeClose = defineLeaf({
|
|
|
105
105
|
const branch = String(r['branch'] ?? '');
|
|
106
106
|
const sha = String(r['landed_sha'] ?? '');
|
|
107
107
|
const path = String(r['worktree_path'] ?? '');
|
|
108
|
-
let base = `Managed worktree
|
|
108
|
+
let base = `Managed worktree closed — ${branch} at ${sha} is retained.`;
|
|
109
109
|
const notes = [];
|
|
110
110
|
if (r['worktree_removed'] === false) {
|
|
111
111
|
notes.push(String(r['worktree_remove_error'] ?? `worktree checkout at ${path} could not be removed automatically.`));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { envProfileId } from '../../shared/env.js';
|
|
2
2
|
import { dirname, join, resolve, isAbsolute, relative, sep } from 'node:path';
|
|
3
|
-
import { renameSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { chmodSync, renameSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { createHash } from 'node:crypto';
|
|
5
5
|
import { defineLeaf } from '../../core/command.js';
|
|
6
6
|
import { notFound, usage, general, network } from '../../core/errors.js';
|
|
@@ -282,6 +282,15 @@ function writeStagedBundle(stagingRoot, manifest, bundle) {
|
|
|
282
282
|
ensureDir(resolve(target, '..'));
|
|
283
283
|
writeFileSync(target, document.bytes);
|
|
284
284
|
}
|
|
285
|
+
if (bundle.hooks !== undefined) {
|
|
286
|
+
writeFileSync(stagedPath(stagingRoot, 'hooks.json'), bundle.hooks.manifest);
|
|
287
|
+
for (const file of bundle.hooks.files) {
|
|
288
|
+
const target = stagedPath(stagingRoot, file.path);
|
|
289
|
+
ensureDir(resolve(target, '..'));
|
|
290
|
+
writeFileSync(target, file.bytes);
|
|
291
|
+
}
|
|
292
|
+
chmodSync(stagedPath(stagingRoot, 'hooks/dispatch'), 0o755);
|
|
293
|
+
}
|
|
285
294
|
}
|
|
286
295
|
async function validateStagedBundle(name, scope, root, scopeRootPath, manifest) {
|
|
287
296
|
const memory = await assertPluginCandidateValid(manifest, root, scope, scopeRootPath, `${root}/.crouter-plugin/plugin.json`);
|
|
@@ -345,6 +354,7 @@ async function replaceBundlePlugin(name, bundleSource, scope, options) {
|
|
|
345
354
|
// later archive that no longer declares them is what retires them.
|
|
346
355
|
...(validated.bundle.page_components !== undefined ? { page_components: validated.bundle.page_components } : {}),
|
|
347
356
|
...(validated.bundle.memory_extensions !== undefined ? { memory_extensions: validated.bundle.memory_extensions } : {}),
|
|
357
|
+
...(validated.bundle.hooks !== undefined ? { hooks: 'hooks.json', hookExecutable: 'hooks/dispatch' } : {}),
|
|
348
358
|
};
|
|
349
359
|
const tmpRoot = join(scopeRootPath, 'tmp');
|
|
350
360
|
const staging = join(tmpRoot, `${name}.${process.pid}`);
|
|
@@ -390,7 +400,8 @@ async function replaceBundlePlugin(name, bundleSource, scope, options) {
|
|
|
390
400
|
const commands = await commandReport(name, scope);
|
|
391
401
|
if (commands === undefined)
|
|
392
402
|
throw general(`bundle plugin "${name}" has no staged command report`);
|
|
393
|
-
|
|
403
|
+
const hooks = await hookLifecycleReport(name, scope);
|
|
404
|
+
return { name, scope, path: root, transport: 'http', version, docs: validated.bundle.memory.length, kinds: Object.keys(validated.bundle.kinds ?? {}).length, pageComponents: (validated.bundle.page_components ?? []).length, commands, ...(hooks !== undefined ? { hooks } : {}), ...(memory !== undefined ? { memory } : {}) };
|
|
394
405
|
}
|
|
395
406
|
async function installHttpPlugin(name, endpoint, authEnv, scope) {
|
|
396
407
|
const source = validateHttpInstall(name, endpoint, authEnv);
|
|
@@ -674,7 +685,7 @@ async function updateOnePlugin(plugin, marketplaceCache, opts) {
|
|
|
674
685
|
const sourceInstalled = isSourceInstalled(plugin);
|
|
675
686
|
if (!sourceInstalled && plugin.manifest.bundle !== undefined) {
|
|
676
687
|
const replaced = await replaceBundlePlugin(plugin.name, plugin.manifest.bundle, plugin.scope, { enable: plugin.enabled });
|
|
677
|
-
return { name: plugin.name, transport: 'http', updated: true, version: replaced.version, docs: replaced.docs, kinds: replaced.kinds, pageComponents: replaced.pageComponents, commands: replaced.commands, ...(replaced.memory !== undefined ? { memory: replaced.memory } : {}) };
|
|
688
|
+
return { name: plugin.name, transport: 'http', updated: true, version: replaced.version, docs: replaced.docs, kinds: replaced.kinds, pageComponents: replaced.pageComponents, commands: replaced.commands, ...(replaced.hooks !== undefined ? { hooks: replaced.hooks } : {}), ...(replaced.memory !== undefined ? { memory: replaced.memory } : {}) };
|
|
678
689
|
}
|
|
679
690
|
if (plugin.manifest.transport?.kind === 'http' && !sourceInstalled)
|
|
680
691
|
rejectLegacyHttpPlugin(plugin.name);
|
|
@@ -729,16 +740,16 @@ export const pluginInstall = defineLeaf({
|
|
|
729
740
|
{ name: 'kinds', type: 'integer', required: false, constraint: 'Kind-registry entry count declared by an --endpoint archive\u2019s bundle.json.' },
|
|
730
741
|
{ name: 'pageComponents', type: 'integer', required: false, constraint: 'Page-component registration count declared by an --endpoint archive\u2019s bundle.json.' },
|
|
731
742
|
{ name: 'commands', type: 'object', required: false, constraint: 'Present only when the plugin declares a command manifest. {mounts: string[] (accepted top-level command names now live for the next invocation), issues: object[] (typed validation issues that rejected a contribution — {code, path?, message, received, expected, next})}. Validated statically; commands are never executed.' },
|
|
732
|
-
{ name: 'hooks', type: 'object', required: false, constraint: 'Present only when the
|
|
743
|
+
{ name: 'hooks', type: 'object', required: false, constraint: 'Present only when the plugin declares hooks. {manifestPath?, executablePath?, declarations: {target, phase, op, description, effects}[], lifecycle?: {event, phase, op, description, effects}[], issues: object[], trust: string}. Command targets, lifecycle events, and replacement collisions are validated statically; hook executables are never run.' },
|
|
733
744
|
{ name: 'warnings', type: 'string[]', required: false, constraint: 'Advisory missing PATH executable requirements declared by the installed plugin. The install still succeeds and leaves the plugin enabled.' },
|
|
734
745
|
{ name: 'memory', type: 'object', required: false, constraint: 'Present only when the plugin ships a memory store. {docs: integer (documents checked), migrated: string[] (store-relative documents rewritten to reach exact canonical identity), linkedSource?: string (the caller-owned directory converged in place, present only when the install keeps that directory as a live link)}.' },
|
|
735
746
|
],
|
|
736
747
|
outputKind: 'object',
|
|
737
748
|
effects: [
|
|
738
749
|
'A ref install clones, links, or copies the plugin into the scope plugins directory and registers it with enabled=true. Marketplace installs refresh the source marketplace before resolving the plugin entry.',
|
|
739
|
-
'An --endpoint install fetches and validates one authenticated uncompressed tar archive before writing. It replaces the complete plugin directory with synthesized provenance and invocation metadata, commands.json, memory docs, any kind-registry entries the bundle.json declares (they join the launch registry below this scope\u2019s own config.json kinds), and any page components it declares (they join the active page-component catalog alongside the user config entries); a fetch or validation failure preserves the prior package.',
|
|
750
|
+
'An --endpoint install fetches and validates one authenticated uncompressed tar archive before writing. It replaces the complete plugin directory with synthesized provenance and invocation metadata, commands.json, memory docs, and, when declared, hooks.json plus the hooks/ tree; it also copies any kind-registry entries the bundle.json declares (they join the launch registry below this scope\u2019s own config.json kinds), and any page components it declares (they join the active page-component catalog alongside the user config entries); a fetch or validation failure preserves the prior package.',
|
|
740
751
|
'If the plugin declares a command manifest, its commands go live on the next crtr invocation. Exec commands run trusted local code only when explicitly invoked; HTTP commands call their declared endpoint only when explicitly invoked.',
|
|
741
|
-
'
|
|
752
|
+
'Plugin command hooks go live on the next crtr invocation. They run implicitly with caller authority, may receive normalized inputs and after results, may block or replace declared targets, and may transmit received data. Lifecycle hooks run unprompted at every declared event before node launch and may transmit node identity and resolved paths. Hook executables are never run during install or update validation.',
|
|
742
753
|
'A plugin manifest `requires` declaration is checked against the PATH a node bash receives after install. Missing executables emit a warning with the declared install hint but never fail the install, disable the plugin, or remove its bare-binary contributions.',
|
|
743
754
|
'Before activation the candidate\u2019s memory corpus is migrated to exact canonical identity and refused if it cannot mount. A staged candidate is migrated in staging and rolls back with the package; a local path or marketplace-relative source stays a live link, so its own directory is rewritten in place, keeps that rewrite whether or not the install completes, and is named as `memory.linkedSource`. A refused corpus writes nothing and installs nothing.',
|
|
744
755
|
],
|
|
@@ -895,7 +906,7 @@ export const pluginDisable = defineLeaf({
|
|
|
895
906
|
export const pluginUpdate = defineLeaf({
|
|
896
907
|
name: 'update',
|
|
897
908
|
description: 'update installed plugin content',
|
|
898
|
-
whenToUse: 'refreshing one named plugin or all installed plugins: archive plugins replace
|
|
909
|
+
whenToUse: 'refreshing one named plugin or all installed plugins: archive plugins replace their complete package, while source-installed plugins retain their source update behavior',
|
|
899
910
|
help: {
|
|
900
911
|
name: 'pkg plugin update',
|
|
901
912
|
summary: 'refresh one or all installed plugins',
|
|
@@ -904,7 +915,7 @@ export const pluginUpdate = defineLeaf({
|
|
|
904
915
|
{ kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'Narrows resolution.' },
|
|
905
916
|
],
|
|
906
917
|
output: [
|
|
907
|
-
{ name: 'updated', type: 'object[]', required: true, constraint: 'One entry per plugin processed: {name, transport?, updated, sha?, version?, docs?, kinds?, pageComponents?, commands?, hooks?, memory?, warnings?, error?}. memory is present for a plugin shipping a memory store and reports the same corpus migration install performs: {docs, migrated, linkedSource?}. Archive plugins report transport=http and replace their complete directory after an authenticated refetch; version is the archive content identifier, docs is the written memory-document count, kinds is the declared kind-registry entry count, and pageComponents is the declared page-component registration count. sha is present for git updates. A bulk update reports and skips a failed archive plugin. commands is present for validated command plugins: {mounts: string[], issues: object[]}. hooks is present for
|
|
918
|
+
{ name: 'updated', type: 'object[]', required: true, constraint: 'One entry per plugin processed: {name, transport?, updated, sha?, version?, docs?, kinds?, pageComponents?, commands?, hooks?, memory?, warnings?, error?}. memory is present for a plugin shipping a memory store and reports the same corpus migration install performs: {docs, migrated, linkedSource?}. Archive plugins report transport=http and replace their complete directory after an authenticated refetch; version is the archive content identifier, docs is the written memory-document count, kinds is the declared kind-registry entry count, and pageComponents is the declared page-component registration count. sha is present for git updates. A bulk update reports and skips a failed archive plugin. commands is present for validated command plugins: {mounts: string[], issues: object[]}. hooks is present for plugins declaring hooks: {manifestPath?, executablePath?, declarations: {target, phase, op, description, effects}[], lifecycle?: {event, phase, op, description, effects}[], issues: object[], trust: string}; it is static and never executes hooks. warnings lists advisory PATH requirements that remain absent after update.' }
|
|
908
919
|
],
|
|
909
920
|
outputKind: 'object',
|
|
910
921
|
effects: [
|
package/dist/commands/push.js
CHANGED
|
@@ -71,7 +71,7 @@ function makePushLeaf(kind) {
|
|
|
71
71
|
...(kind === 'final'
|
|
72
72
|
? [
|
|
73
73
|
'Marks the node done (status + intent) server-side; its engine shuts down on next stop.',
|
|
74
|
-
'When this node owns a managed worktree
|
|
74
|
+
'When this node owns a clean managed worktree already contained in its local base, the server auto-drops it instead of blocking on `node worktree close`. A clean worktree whose exact branch tip is already on origin may also finalize while retaining its branch and checkout; uncommitted changes or a branch tip not on origin still block with open_managed_worktree.',
|
|
75
75
|
]
|
|
76
76
|
: []),
|
|
77
77
|
],
|
|
@@ -123,6 +123,18 @@ async function waitFor(probe, opts = {}) {
|
|
|
123
123
|
await new Promise((r) => setTimeout(r, intervalMs));
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
|
+
export function readLines(path) {
|
|
127
|
+
try {
|
|
128
|
+
const content = readFileSync(path, 'utf8');
|
|
129
|
+
const lines = content.split('\n');
|
|
130
|
+
if (!content.endsWith('\n'))
|
|
131
|
+
lines.pop();
|
|
132
|
+
return lines.filter((l) => l.trim() !== '');
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
}
|
|
126
138
|
/** Thin wrapper: a headless-only harness (broker-hosted, paneless nodes). */
|
|
127
139
|
export async function createHeadlessHarness(opts = {}) {
|
|
128
140
|
return createHarness({ ...opts, headless: true });
|
|
@@ -377,16 +389,6 @@ export async function createHarness(opts = {}) {
|
|
|
377
389
|
return [];
|
|
378
390
|
}
|
|
379
391
|
}
|
|
380
|
-
function readLines(path) {
|
|
381
|
-
try {
|
|
382
|
-
return readFileSync(path, 'utf8')
|
|
383
|
-
.split('\n')
|
|
384
|
-
.filter((l) => l.trim() !== '');
|
|
385
|
-
}
|
|
386
|
-
catch {
|
|
387
|
-
return [];
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
392
|
function cli(nodeId, args) {
|
|
391
393
|
const runtimeArgs = TSX_ESM === null ? [] : ['--import', TSX_ESM];
|
|
392
394
|
const res = spawnSync(process.execPath, [...runtimeArgs, CLI_ENTRY, ...args], {
|
|
@@ -440,8 +440,13 @@ test('close concludes a clean child after parent-first closure removes its check
|
|
|
440
440
|
run(['commit', '-m', 'init'], repo);
|
|
441
441
|
const parent = createManagedWorktree(repo, 'wt-parent-first-parent');
|
|
442
442
|
createNode(node('wt-parent-first-parent', { cwd: parent.path, managed_worktree: parent }));
|
|
443
|
-
|
|
444
|
-
|
|
443
|
+
// Creation now refuses a managed worktree as its base (`managed_worktree_base`),
|
|
444
|
+
// so the child is cut from the real repository against the parent's BRANCH, and
|
|
445
|
+
// its record carries the parent checkout as `repo_root` — the exact legacy shape
|
|
446
|
+
// this case is about, still on disk for every record created before that guard.
|
|
447
|
+
const child = createManagedWorktree(repo, 'wt-parent-first-child', parent.branch);
|
|
448
|
+
const childRecord = { ...child, repo_root: parent.path };
|
|
449
|
+
createNode(node('wt-parent-first-child', { cwd: child.path, managed_worktree: childRecord }));
|
|
445
450
|
assert.equal(gitSync(['rev-parse', 'HEAD'], child.path).stdout.trim(), child.base_sha, 'the child must be a clean zero-commit checkout at its recorded base');
|
|
446
451
|
closeManagedWorktree('wt-parent-first-parent');
|
|
447
452
|
run(['worktree', 'remove', parent.path], repo);
|
|
@@ -9,7 +9,7 @@ import { test, before, after, afterEach } from 'node:test';
|
|
|
9
9
|
import assert from 'node:assert/strict';
|
|
10
10
|
import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
|
|
11
11
|
import { join } from 'node:path';
|
|
12
|
-
import { createHeadlessHarness } from '../helpers/harness.js';
|
|
12
|
+
import { createHeadlessHarness, readLines } from '../helpers/harness.js';
|
|
13
13
|
import { createAttachKit } from '../helpers/broker-clients.js';
|
|
14
14
|
import { defaultModelLaddersConfig } from '../../../types.js';
|
|
15
15
|
import { admitProviderRetryEpisode, readFault, readProviderRetryEpisode } from '../../runtime/fault.js';
|
|
@@ -63,21 +63,8 @@ function overflowOutcome(id) {
|
|
|
63
63
|
// re-drive is a fresh session.prompt → emitTurn, which fires one 'agent_start';
|
|
64
64
|
// counting them is the observable proof of how many times the turn was re-driven.
|
|
65
65
|
function agentStartCount(id) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
.split('\n')
|
|
69
|
-
.filter((l) => {
|
|
70
|
-
try {
|
|
71
|
-
return JSON.parse(l).event === 'agent_start';
|
|
72
|
-
}
|
|
73
|
-
catch {
|
|
74
|
-
return false;
|
|
75
|
-
}
|
|
76
|
-
}).length;
|
|
77
|
-
}
|
|
78
|
-
catch {
|
|
79
|
-
return 0;
|
|
80
|
-
}
|
|
66
|
+
return readLines(join(h.home, 'nodes', id, 'fake-pi.events.jsonl'))
|
|
67
|
+
.filter((line) => JSON.parse(line).event === 'agent_start').length;
|
|
81
68
|
}
|
|
82
69
|
async function crashAndRespawn(id, boots) {
|
|
83
70
|
const pid = h.node(id)?.pi_pid;
|