@north-light/crouter 0.3.236 → 0.3.237
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/api/client.d.ts +2 -1
- package/dist/api/client.js +3 -0
- package/dist/api/dto/broker-ops.d.ts +1 -1
- package/dist/api/dto/common.d.ts +2 -0
- package/dist/api/dto/lifecycle.d.ts +9 -2
- package/dist/api/dto/messages.d.ts +5 -0
- package/dist/api/dto/nodes.d.ts +10 -2
- package/dist/api/dto/worktree.d.ts +10 -0
- package/dist/api/routes.d.ts +1 -0
- package/dist/api/routes.js +1 -0
- package/dist/builtin-memory/00-runtime-base/00-authoring.md +1 -1
- package/dist/builtin-memory/internal/agent-shaping.md +1 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/README.md +1 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/sysprompt-window.ts +1 -1
- package/dist/clients/attach/render/tool-calls.js +18 -3
- package/dist/clients/attach/viewer.js +426 -426
- package/dist/clients/inbox/review/__tests__/editor-roundtrip.test.js +6 -0
- package/dist/clients/inbox/review/roundtrip.js +1 -1
- package/dist/commands/memory/lint.js +22 -13
- package/dist/commands/memory/shared.js +1 -1
- package/dist/commands/node/create.d.ts +1 -1
- package/dist/commands/node/create.js +1 -1
- package/dist/commands/node/lifecycle.js +1 -1
- package/dist/commands/node-worktree.js +39 -8
- package/dist/commands/revive.js +11 -2
- package/dist/commands/sys/context/admin/actions.d.ts +48 -0
- package/dist/commands/sys/context/admin/actions.js +246 -0
- package/dist/commands/sys/context/admin/axes-panel.d.ts +21 -0
- package/dist/commands/sys/context/admin/axes-panel.js +117 -0
- package/dist/commands/sys/context/admin/detail-panel.d.ts +53 -0
- package/dist/commands/sys/context/admin/detail-panel.js +178 -0
- package/dist/commands/sys/context/admin/docs-panel.d.ts +45 -0
- package/dist/commands/sys/context/admin/docs-panel.js +186 -0
- package/dist/commands/sys/context/admin/model.d.ts +75 -0
- package/dist/commands/sys/context/admin/model.js +209 -0
- package/dist/commands/sys/context/admin/shell.d.ts +82 -0
- package/dist/commands/sys/context/admin/shell.js +669 -0
- package/dist/commands/sys/context/admin.d.ts +1 -0
- package/dist/commands/sys/context/admin.js +124 -0
- package/dist/commands/sys/context/doc.d.ts +1 -0
- package/dist/commands/sys/context/doc.js +196 -0
- package/dist/commands/sys/context/prompt-review.d.ts +1 -0
- package/dist/commands/sys/{prompt-review.js → context/prompt-review.js} +10 -10
- package/dist/commands/sys/context/resolve.d.ts +55 -0
- package/dist/commands/sys/context/resolve.js +428 -0
- package/dist/commands/sys/context/sysprompt.d.ts +1 -0
- package/dist/commands/sys/{sysprompt.js → context/sysprompt.js} +11 -11
- package/dist/commands/sys/context.d.ts +2 -0
- package/dist/commands/sys/context.js +20 -0
- package/dist/commands/sys.js +3 -4
- package/dist/core/__tests__/child-death-wake.test.js +38 -22
- package/dist/core/__tests__/cron-self-cancel-delivery.test.js +72 -0
- package/dist/core/__tests__/daemon-boot.test.js +40 -28
- package/dist/core/__tests__/human-deliver.test.js +1 -1
- package/dist/core/__tests__/human-node-not-supervised.test.js +10 -15
- package/dist/core/__tests__/integration/revive.test.js +12 -12
- package/dist/core/__tests__/integration/spawn-root.test.js +6 -0
- package/dist/core/__tests__/integration/worktree-reap.test.js +32 -0
- package/dist/core/__tests__/lifecycle-terminal-reasons.test.d.ts +1 -0
- package/dist/core/__tests__/lifecycle-terminal-reasons.test.js +61 -0
- package/dist/core/__tests__/migration.test.js +38 -1
- package/dist/core/__tests__/push-final-guard.test.js +1 -1
- package/dist/core/__tests__/revive-capacity.test.d.ts +1 -0
- package/dist/core/__tests__/revive-capacity.test.js +127 -0
- package/dist/core/__tests__/revive-parked-fresh.test.js +7 -7
- package/dist/core/__tests__/seam/broker-cap-freeze.test.d.ts +1 -0
- package/dist/core/__tests__/seam/broker-cap-freeze.test.js +81 -0
- package/dist/core/__tests__/seam/dormancy-release.test.js +7 -7
- package/dist/core/__tests__/seam/yield-refresh-transaction.test.js +1 -1
- package/dist/core/canvas/browse/render.js +11 -3
- package/dist/core/canvas/canvas.d.ts +15 -3
- package/dist/core/canvas/canvas.js +52 -16
- package/dist/core/canvas/migrations.js +21 -0
- package/dist/core/canvas/nav-render.js +3 -1
- package/dist/core/canvas/remote-canvas-source.js +2 -0
- package/dist/core/canvas/render-source.d.ts +3 -1
- package/dist/core/canvas/render-source.js +7 -3
- package/dist/core/canvas/status-glyph.d.ts +10 -1
- package/dist/core/canvas/status-glyph.js +19 -3
- package/dist/core/canvas/types.d.ts +20 -1
- package/dist/core/feed/feed.js +1 -1
- package/dist/core/human/feedback-companion.js +5 -2
- package/dist/core/memory/lint.d.ts +19 -0
- package/dist/core/memory/lint.js +97 -0
- package/dist/core/memory-resolver.d.ts +4 -0
- package/dist/core/memory-resolver.js +7 -7
- package/dist/core/preview-registry.js +4 -4
- package/dist/core/review/realize.js +4 -2
- package/dist/core/runtime/close.js +1 -1
- package/dist/core/runtime/fleet.d.ts +29 -1
- package/dist/core/runtime/fleet.js +23 -0
- package/dist/core/runtime/host.js +32 -11
- package/dist/core/runtime/launch-prompt.d.ts +1 -1
- package/dist/core/runtime/launch-prompt.js +2 -2
- package/dist/core/runtime/lifecycle.d.ts +4 -2
- package/dist/core/runtime/lifecycle.js +12 -1
- package/dist/core/runtime/memory.js +1 -1
- package/dist/core/runtime/reopen.js +10 -1
- package/dist/core/runtime/reset.js +1 -1
- package/dist/core/runtime/revive-all.d.ts +3 -0
- package/dist/core/runtime/revive-all.js +10 -4
- package/dist/core/runtime/revive.d.ts +12 -0
- package/dist/core/runtime/revive.js +62 -3
- package/dist/core/runtime/spawn.d.ts +4 -0
- package/dist/core/runtime/spawn.js +36 -7
- package/dist/core/substrate/frontmatter-validation.js +1 -1
- package/dist/core/substrate/index.d.ts +6 -3
- package/dist/core/substrate/index.js +4 -3
- package/dist/core/substrate/on-read.d.ts +0 -8
- package/dist/core/substrate/on-read.js +45 -124
- package/dist/core/substrate/plan.d.ts +94 -0
- package/dist/core/substrate/plan.js +266 -0
- package/dist/core/substrate/render.d.ts +5 -6
- package/dist/core/substrate/render.js +76 -129
- package/dist/core/substrate/schema.d.ts +1 -6
- package/dist/core/substrate/schema.js +1 -15
- package/dist/core/substrate/session-cache.d.ts +2 -10
- package/dist/core/substrate/session-cache.js +9 -47
- package/dist/core/substrate/subject-fields.d.ts +1 -4
- package/dist/core/substrate/subject-fields.js +2 -5
- package/dist/core/substrate/surface-match.d.ts +37 -17
- package/dist/core/substrate/surface-match.js +44 -53
- package/dist/core/tui/page-host.d.ts +6 -0
- package/dist/core/tui/page-host.js +12 -3
- package/dist/core/worktree.d.ts +8 -0
- package/dist/core/worktree.js +65 -0
- package/dist/daemon/__tests__/reconciler-signature.test.js +3 -2
- package/dist/daemon/__tests__/thaw-order.test.d.ts +1 -0
- package/dist/daemon/__tests__/thaw-order.test.js +55 -0
- package/dist/daemon/api/__tests__/canvas-snapshot-fields.test.js +6 -0
- package/dist/daemon/api/__tests__/node-create-description.test.js +6 -0
- package/dist/daemon/api/__tests__/profile-launch-gates.test.js +1 -1
- package/dist/daemon/api/bridge.js +5 -1
- package/dist/daemon/api/handlers/attach.js +5 -1
- package/dist/daemon/api/handlers/bash-jobs.js +1 -1
- package/dist/daemon/api/handlers/messages.js +10 -2
- package/dist/daemon/api/handlers/nodes.js +8 -1
- package/dist/daemon/api/handlers/worktree.js +10 -1
- package/dist/daemon/api/map.js +7 -0
- package/dist/daemon/companion-retire.js +1 -1
- package/dist/daemon/cron/passes.d.ts +0 -5
- package/dist/daemon/cron/passes.js +0 -20
- package/dist/daemon/cron/sinks.js +1 -1
- package/dist/daemon/cron-run.d.ts +5 -6
- package/dist/daemon/cron-run.js +7 -16
- package/dist/daemon/crtrd.d.ts +0 -10
- package/dist/daemon/crtrd.js +23 -61
- package/dist/daemon/fleet.d.ts +10 -7
- package/dist/daemon/fleet.js +71 -12
- package/dist/daemon/messaging/node-message.js +3 -1
- package/dist/daemon/profile-delete.js +1 -1
- package/dist/daemon/reconcilers/broker-supervision.d.ts +26 -9
- package/dist/daemon/reconcilers/broker-supervision.js +39 -52
- package/dist/daemon/reconcilers/controller-death.d.ts +7 -0
- package/dist/daemon/reconcilers/controller-death.js +26 -0
- package/dist/daemon/reconcilers/cron-lane.d.ts +0 -3
- package/dist/daemon/reconcilers/cron-lane.js +0 -3
- package/dist/daemon/reconcilers/live-obligation.js +3 -1
- package/dist/daemon/reconcilers/node-lifecycle/freeze-lane.d.ts +53 -0
- package/dist/daemon/reconcilers/node-lifecycle/freeze-lane.js +170 -0
- package/dist/daemon/reconcilers/node-lifecycle/respawn-policy.d.ts +5 -0
- package/dist/daemon/reconcilers/node-lifecycle/respawn-policy.js +16 -0
- package/dist/daemon/reconcilers/node-lifecycle/terminating.d.ts +4 -0
- package/dist/daemon/reconcilers/node-lifecycle/terminating.js +13 -0
- package/dist/daemon/reconcilers/node-lifecycle/tick.d.ts +26 -0
- package/dist/daemon/reconcilers/node-lifecycle/tick.js +140 -0
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
- package/dist/commands/sys/prompt-review.d.ts +0 -1
- package/dist/commands/sys/sysprompt.d.ts +0 -1
- package/dist/core/__tests__/cron-broker-capacity.test.js +0 -119
- package/dist/daemon/reconcilers/dormant-inbox.d.ts +0 -18
- package/dist/daemon/reconcilers/dormant-inbox.js +0 -157
- /package/dist/core/__tests__/{cron-broker-capacity.test.d.ts → cron-self-cancel-delivery.test.d.ts} +0 -0
|
@@ -5,6 +5,8 @@ import { join } from 'node:path';
|
|
|
5
5
|
import { editBufferInEditor } from '../roundtrip.js';
|
|
6
6
|
const temp = mkdtempSync(join(tmpdir(), 'humanloop-editor-roundtrip-'));
|
|
7
7
|
const priorEditor = process.env.EDITOR;
|
|
8
|
+
const priorVisual = process.env.VISUAL;
|
|
9
|
+
delete process.env.VISUAL;
|
|
8
10
|
try {
|
|
9
11
|
const successfulEditor = join(temp, 'success-editor');
|
|
10
12
|
writeFileSync(successfulEditor, '#!/bin/sh\nprintf "edited\\n" > "$1"\n');
|
|
@@ -24,6 +26,10 @@ finally {
|
|
|
24
26
|
delete process.env.EDITOR;
|
|
25
27
|
else
|
|
26
28
|
process.env.EDITOR = priorEditor;
|
|
29
|
+
if (priorVisual === undefined)
|
|
30
|
+
delete process.env.VISUAL;
|
|
31
|
+
else
|
|
32
|
+
process.env.VISUAL = priorVisual;
|
|
27
33
|
rmSync(temp, { recursive: true, force: true });
|
|
28
34
|
}
|
|
29
35
|
console.log('editor round-trip tests passed');
|
|
@@ -6,7 +6,7 @@ import { join } from 'node:path';
|
|
|
6
6
|
/** Run $EDITOR against a temporary buffer. Terminal ownership stays with the host. */
|
|
7
7
|
export function editBufferInEditor(buffer) {
|
|
8
8
|
const tmpFile = join(tmpdir(), `crtr-input-${randomUUID()}.txt`);
|
|
9
|
-
const editor = process.env.EDITOR || 'vi';
|
|
9
|
+
const editor = process.env.VISUAL || process.env.EDITOR || 'vi';
|
|
10
10
|
try {
|
|
11
11
|
writeFileSync(tmpFile, buffer);
|
|
12
12
|
// $EDITOR may be a shell fragment such as "code --wait".
|
|
@@ -7,7 +7,7 @@ import { envProfileId } from '../../shared/env.js';
|
|
|
7
7
|
import { defineLeaf } from '../../core/command.js';
|
|
8
8
|
import { warn } from '../../core/output.js';
|
|
9
9
|
import { ExitCode } from '../../types.js';
|
|
10
|
-
import { lintMemoryStore, lintMemoryTarget, resolvableCanonicalNames, } from '../../core/memory/lint.js';
|
|
10
|
+
import { lintMemoryStore, lintMemoryTarget, lintOrphans, resolvableCanonicalNames, } from '../../core/memory/lint.js';
|
|
11
11
|
import { loadMemoryTargetView } from '../../core/memory-resolver.js';
|
|
12
12
|
import { resolveReadSelector, selectorParam } from './shared.js';
|
|
13
13
|
function findingRow(finding) {
|
|
@@ -21,16 +21,16 @@ function findingRow(finding) {
|
|
|
21
21
|
...(finding.paths === undefined ? {} : { paths: [...finding.paths] }),
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
|
-
/** The
|
|
25
|
-
*
|
|
26
|
-
* isolation — a project doc legitimately links a user or
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
/** The target view an agent standing in a `--dir` store's project sees. Both a
|
|
25
|
+
* project doc's links/routes and its reachability are resolved against this,
|
|
26
|
+
* never the store in isolation — a project doc legitimately links a user or
|
|
27
|
+
* builtin doc, and is legitimately reached from one. */
|
|
28
|
+
function targetViewForStore(store) {
|
|
29
|
+
return loadMemoryTargetView({
|
|
29
30
|
cwd: store.ownerDir ?? store.storeRoot,
|
|
30
31
|
profileId: envProfileId() || null,
|
|
31
32
|
nodeId: null,
|
|
32
33
|
}, { quiet: true, includeDescendants: true });
|
|
33
|
-
return resolvableCanonicalNames(view.docs);
|
|
34
34
|
}
|
|
35
35
|
export const lintLeaf = defineLeaf({
|
|
36
36
|
name: 'lint',
|
|
@@ -48,7 +48,7 @@ export const lintLeaf = defineLeaf({
|
|
|
48
48
|
{ name: 'stores', type: 'number', required: true, constraint: 'Physical memory stores linted.' },
|
|
49
49
|
{ name: 'profile_projects', type: 'number', required: false, constraint: 'Projects the selected profile manages that were checked for a workspace front door — zero under a scope filter excluding project stores. Absent under --dir.' },
|
|
50
50
|
{ name: 'findings', type: 'object[]', required: true, constraint: 'One row per ERROR: {rule, path, message, name?, scope?, store_root?, paths?}. `rule` names the violated rule (yaml, schema, extension, length, store-namespace, namespace-placement, root-name, root-index-unaddressable, local-name, canonical-collision, dangling-link, memory-read-route, front-door, owner-path-collision). `paths` carries every file sharing a collided identity, read winner first. Empty when green.' },
|
|
51
|
-
{ name: 'warnings', type: 'object[]', required: true, constraint: 'Same row shape, non-fatal authoring smells: short-preview, broad-memory-read, nested-store-boot, an advisory missing front door, a wildcard memory-read glob matching nothing.' },
|
|
51
|
+
{ name: 'warnings', type: 'object[]', required: true, constraint: 'Same row shape, non-fatal authoring smells: short-preview, broad-memory-read, nested-store-boot, an advisory missing front door, a wildcard memory-read glob matching nothing, and orphan (a doc no reachability path routes an agent to — unfiltered target/--dir view only, never under --scope).' },
|
|
52
52
|
{ name: 'notices', type: 'object[]', required: true, constraint: 'Same row shape, inspection data rather than faults: cross-store-candidates (one canonical name held by several stores, winner first) and reference-check-deferred.' },
|
|
53
53
|
{ name: 'follow_up', type: 'string', required: true, constraint: 'Concrete next command.' },
|
|
54
54
|
],
|
|
@@ -60,14 +60,23 @@ export const lintLeaf = defineLeaf({
|
|
|
60
60
|
scope: input['scope'],
|
|
61
61
|
dir: input['dir'],
|
|
62
62
|
});
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
let result;
|
|
64
|
+
if (selector.store !== null) {
|
|
65
|
+
const view = targetViewForStore(selector.store);
|
|
66
|
+
const base = lintMemoryStore(selector.store, {
|
|
67
|
+
resolvable: resolvableCanonicalNames(view.docs),
|
|
66
68
|
// Asked about directly, not as a profile-managed project: a missing
|
|
67
69
|
// front door is a gap to report, not a failed gate.
|
|
68
70
|
frontDoor: 'advisory',
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
+
});
|
|
72
|
+
// Reachability is a whole-view property, so orphans are computed over the
|
|
73
|
+
// project's target view but flagged only for this store's own docs.
|
|
74
|
+
const orphans = lintOrphans(view.docs, { flagStoreRoot: selector.store.storeRoot });
|
|
75
|
+
result = { ...base, findings: [...base.findings, ...orphans], warnings: [...base.warnings, ...orphans] };
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
result = lintMemoryTarget({ ...(selector.scope === undefined ? {} : { scope: selector.scope }) });
|
|
79
|
+
}
|
|
71
80
|
const errors = result.errors.map(findingRow);
|
|
72
81
|
const warnings = result.warnings.map(findingRow);
|
|
73
82
|
const notices = result.notices.map(findingRow);
|
|
@@ -778,7 +778,7 @@ export const FRONTMATTER_OVERLAY_PARAMS = {
|
|
|
778
778
|
'unlisted': { kind: 'flag', name: 'unlisted', type: 'bool', required: false, default: false, constraint: 'Suppress this doc from directory listings. Suppression only — explicit reads, [[links]], and surfaces entries still work.' },
|
|
779
779
|
'surface': { kind: 'flag', name: 'surface', type: 'string', required: false, repeatable: true, constraint: 'One routing entry per occurrence, as a YAML/JSON object `{on, at, match?, match-frontmatter?, gate?}`; the flag set replaces the document’s whole surfaces list. `on` is boot|workspace-open|read|memory-read|command|pre-command; `at` is name|preview|content. `match` holds the event’s globs — required on read/memory-read/command/pre-command (a read entry may carry `match-frontmatter`, a predicate over the read file’s own frontmatter, instead), meaningless on boot/workspace-open. Optional entry `gate` is a node-config predicate using the same vocabulary as the document gate; its event constraints and gate both match before it participates. A `./`-anchored glob is relative: for `read` to the store’s owning repo dir, for `memory-read` to this doc’s routing anchor — its own canonical name when the doc is its directory’s document (`<dir>/INDEX.md`), otherwise the canonical directory it sits in. Participating entries fold to their highest `at`; there is no cross-entry deny precedence.' },
|
|
780
780
|
'gate': { kind: 'flag', name: 'gate', type: 'string', required: false, constraint: 'Frontmatter gate \u2014 YAML/JSON object predicate over node config using the same field/matcher vocabulary described in the guide.' },
|
|
781
|
-
'lint-ignore': { kind: 'flag', name: 'lint-ignore', type: 'enum', choices: [...SUPPRESSIBLE_RULES], required: false, repeatable: true, constraint: 'Suppress one authoring lint rule for this document. Repeat for a list; valid rules: length, broad-memory-read.' },
|
|
781
|
+
'lint-ignore': { kind: 'flag', name: 'lint-ignore', type: 'enum', choices: [...SUPPRESSIBLE_RULES], required: false, repeatable: true, constraint: 'Suppress one authoring lint rule for this document. Repeat for a list; valid rules: length, broad-memory-read, orphan.' },
|
|
782
782
|
'slash': { kind: 'flag', name: 'slash', type: 'bool', required: false, default: false, constraint: 'Presence flags this doc invocable as a pi slash command (`/<name>`, `/` in a nested name rendered as `:`) \u2014 the doc body becomes the command\u2019s injected prompt. Default false: most docs are consulted, not invoked.' },
|
|
783
783
|
};
|
|
784
784
|
/** One overlay flag, with leaf-specific prose appended to the shared
|
|
@@ -18,6 +18,6 @@ export declare function assertKind(kind: string): void;
|
|
|
18
18
|
* sub-kind (universally available) earns a place in this list. A direct launch
|
|
19
19
|
* by full sub-kind string is always valid regardless (visibility-only
|
|
20
20
|
* filtering) — discover the roster for a specific kind via
|
|
21
|
-
* `crtr sys prompt-review --list --kind <kind>`. */
|
|
21
|
+
* `crtr sys context prompt-review --list --kind <kind>`. */
|
|
22
22
|
export declare function kindsStateBlock(): string;
|
|
23
23
|
export declare const nodeNew: import("../../core/command.js").LeafDef;
|
|
@@ -51,7 +51,7 @@ export function assertKind(kind) {
|
|
|
51
51
|
* sub-kind (universally available) earns a place in this list. A direct launch
|
|
52
52
|
* by full sub-kind string is always valid regardless (visibility-only
|
|
53
53
|
* filtering) — discover the roster for a specific kind via
|
|
54
|
-
* `crtr sys prompt-review --list --kind <kind>`. */
|
|
54
|
+
* `crtr sys context prompt-review --list --kind <kind>`. */
|
|
55
55
|
export function kindsStateBlock() {
|
|
56
56
|
const registry = readMergedLaunchConfig().kinds;
|
|
57
57
|
const kinds = Object.keys(registry)
|
|
@@ -246,7 +246,7 @@ export const nodePromote = defineLeaf({
|
|
|
246
246
|
name: 'node promote',
|
|
247
247
|
summary: 'promote yourself to an orchestrator when worthwhile parallel delegation should make coordination your primary job; task duration, sequential phases, or one helper do not qualify. Mode only — lifecycle stays as-is unless you pass --resident',
|
|
248
248
|
params: [
|
|
249
|
-
{ kind: 'flag', name: 'kind', type: 'string', required: false, constraint: 'Specialize as this kind of orchestrator. The <kinds> list below names every top-level installable kind and when to use each; a registered sub-kind is valid too by exact path but not listed here. For an exact composed persona list, use `crtr sys prompt-review --list --kind <kind>` with the target kind. Defaults to your current kind. Promoting from a generic kind? CHOOSE a concrete one — it sets the orchestrator persona you revive into.' },
|
|
249
|
+
{ kind: 'flag', name: 'kind', type: 'string', required: false, constraint: 'Specialize as this kind of orchestrator. The <kinds> list below names every top-level installable kind and when to use each; a registered sub-kind is valid too by exact path but not listed here. For an exact composed persona list, use `crtr sys context prompt-review --list --kind <kind>` with the target kind. Defaults to your current kind. Promoting from a generic kind? CHOOSE a concrete one — it sets the orchestrator persona you revive into.' },
|
|
250
250
|
{ kind: 'flag', name: 'resident', type: 'bool', required: false, constraint: 'ALSO flip lifecycle→resident: make the node interactable — it is never forced to submit a final, and unattended grace parks it after a summary turn instead, leaving it revivable fresh against its goal and roadmap. Omit to stay terminal/orchestrator (delegates + holds a roadmap, but still owes a final up the spine and reaps when done).' },
|
|
251
251
|
{ kind: 'flag', name: 'model', type: 'enum', choices: ['ultra', 'strong'], required: false, constraint: 'Change the model you run on, by capability tier: `ultra` (frontier — reserve for high-taste judgment or enormous work: speccing, planning something large, e2e-testing something hard to test) or `strong` (opus — regular dev work). Omit to keep your current model. An orchestrator steering work is never weaker than opus, so these are the only two choices; the change is durable across future revives.' },
|
|
252
252
|
{ kind: 'flag', name: 'node', type: 'string', required: false, constraint: 'Node to promote. Defaults to the caller (CRTR_NODE_ID).' },
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
// `crtr node worktree`
|
|
2
|
-
// `node new --worktree <base>`; this branch holds the single close
|
|
3
|
-
// verb, symmetric with birth, so a git worktree's whole lifecycle sits under
|
|
4
|
-
// `node`.
|
|
1
|
+
// `crtr node worktree` manages node-owned managed worktrees.
|
|
5
2
|
import { envNodeId } from '../shared/env.js';
|
|
6
3
|
import { defineBranch, defineLeaf } from '../core/command.js';
|
|
7
4
|
import { InputError } from '../core/io.js';
|
|
@@ -39,6 +36,39 @@ function mapWorktreeApiError(err) {
|
|
|
39
36
|
}
|
|
40
37
|
throw err;
|
|
41
38
|
}
|
|
39
|
+
const worktreeAbandon = defineLeaf({
|
|
40
|
+
name: 'abandon',
|
|
41
|
+
description: 'remove a node worktree and preserve any branch work',
|
|
42
|
+
whenToUse: 'a canceled, dead, or otherwise finished node holds work you will not land now: remove its checkout without reviving it, retain its branch work, and release its worktree ownership so finalization or deletion can continue',
|
|
43
|
+
help: {
|
|
44
|
+
name: 'node worktree abandon',
|
|
45
|
+
summary: 'remove a node-managed checkout, retain its work, and commit dirty changes first',
|
|
46
|
+
params: [
|
|
47
|
+
{ kind: 'positional', name: 'node', required: true, constraint: 'Node id whose open managed worktree to abandon.' },
|
|
48
|
+
],
|
|
49
|
+
output: [
|
|
50
|
+
{ name: 'node_id', type: 'string', required: true, constraint: 'The node whose worktree was abandoned.' },
|
|
51
|
+
{ name: 'branch', type: 'string', required: true, constraint: 'The branch that retains abandoned work, or the deleted empty branch.' },
|
|
52
|
+
{ name: 'tip_sha', type: 'string', required: true, constraint: 'The preserved work tip, or the deleted empty branch tip.' },
|
|
53
|
+
{ name: 'worktree_path', type: 'string', required: true, constraint: 'The removed managed checkout path.' },
|
|
54
|
+
{ name: 'branch_deleted', type: 'boolean', required: true, constraint: 'Whether the empty branch was deleted; false means the branch retains work or cleanup needs manual completion.' },
|
|
55
|
+
],
|
|
56
|
+
outputKind: 'object',
|
|
57
|
+
effects: ['Removes the checkout. A clean zero-commit worktree deletes its empty branch. Otherwise commits all worktree state to the branch, retains that branch, records abandoned provenance, and repoints the node cwd to the repository root without changing lifecycle status.'],
|
|
58
|
+
},
|
|
59
|
+
run: async (input) => {
|
|
60
|
+
const nodeId = input['node'];
|
|
61
|
+
try {
|
|
62
|
+
return { ...(await cliClient().abandonWorktree(nodeId, { by: envNodeId() ?? 'human' })) };
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
mapWorktreeApiError(err);
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
render: (r) => r['branch_deleted'] === true
|
|
69
|
+
? `Managed empty worktree abandoned — checkout and empty branch ${String(r['branch'])} removed.`
|
|
70
|
+
: `Managed worktree abandoned — checkout removed; branch ${String(r['branch'])} kept at ${String(r['tip_sha'])}.`,
|
|
71
|
+
});
|
|
42
72
|
const worktreeClose = defineLeaf({
|
|
43
73
|
name: 'close',
|
|
44
74
|
description: 'land and close the current node-managed worktree',
|
|
@@ -93,11 +123,12 @@ const worktreeClose = defineLeaf({
|
|
|
93
123
|
});
|
|
94
124
|
export const nodeWorktreeBranch = defineBranch({
|
|
95
125
|
name: 'worktree',
|
|
96
|
-
description: 'manage a node-owned git worktree —
|
|
97
|
-
whenToUse: 'you
|
|
126
|
+
description: 'manage a node-owned git worktree — land it or explicitly abandon it',
|
|
127
|
+
whenToUse: 'you need to dispose of a managed checkout: close lands the current node’s work onto its base; abandon removes any node’s checkout while retaining its work on the branch',
|
|
98
128
|
help: {
|
|
99
129
|
name: 'node worktree',
|
|
100
|
-
summary: 'manage a node-owned git worktree —
|
|
130
|
+
summary: 'manage a node-owned git worktree — land it, or explicitly abandon its checkout while retaining work',
|
|
131
|
+
model: 'Close merges work into its recorded base. Abandon removes the checkout without changing lifecycle state; it retains work on the branch and deletes only a clean zero-commit branch.',
|
|
101
132
|
},
|
|
102
|
-
children: [worktreeClose],
|
|
133
|
+
children: [worktreeClose, worktreeAbandon],
|
|
103
134
|
});
|
package/dist/commands/revive.js
CHANGED
|
@@ -62,6 +62,7 @@ export const reviveLeaf = defineLeaf({
|
|
|
62
62
|
{ name: 'candidates', type: 'string', required: false, constraint: 'Preview: the node ids that WOULD be revived (newline-joined). Empty when none are disconnected.' },
|
|
63
63
|
{ name: 'revived', type: 'string', required: false, constraint: 'Sweep: the node ids whose broker engine was relaunched (newline-joined).' },
|
|
64
64
|
{ name: 'failed', type: 'string', required: false, constraint: 'Sweep: node ids whose relaunch threw, with the error (newline-joined). Absent when none failed.' },
|
|
65
|
+
{ name: 'frozen', type: 'string', required: false, constraint: 'Sweep: node ids the live-broker cap turned away (newline-joined). Not failures — the daemon launches each as a slot frees. Absent when none were frozen.' },
|
|
65
66
|
],
|
|
66
67
|
outputKind: 'object',
|
|
67
68
|
effects: [
|
|
@@ -164,9 +165,17 @@ async function runReviveAll(input) {
|
|
|
164
165
|
};
|
|
165
166
|
if (res.failed.length > 0) {
|
|
166
167
|
out['failed'] = res.failed.map((f) => `${f.node_id}: ${f.error}`).join('\n');
|
|
168
|
+
}
|
|
169
|
+
if (res.frozen.length > 0) {
|
|
170
|
+
out['frozen'] = res.frozen.join('\n');
|
|
171
|
+
}
|
|
172
|
+
const total = res.revived.length + res.failed.length + res.frozen.length;
|
|
173
|
+
if (total > res.revived.length) {
|
|
167
174
|
out['message'] =
|
|
168
|
-
`Revived ${res.revived.length} of ${
|
|
169
|
-
|
|
175
|
+
`Revived ${res.revived.length} of ${total} disconnected node(s) (RESUME)` +
|
|
176
|
+
(res.failed.length > 0 ? `; ${res.failed.length} failed to relaunch (see failed)` : '') +
|
|
177
|
+
(res.frozen.length > 0 ? `; ${res.frozen.length} frozen at the broker cap and launched as slots free (see frozen)` : '') +
|
|
178
|
+
'.';
|
|
170
179
|
}
|
|
171
180
|
return out;
|
|
172
181
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { DeliveryRecord } from '../../../../core/substrate/plan.js';
|
|
2
|
+
import type { Rung, SurfaceEntry } from '../../../../core/substrate/schema.js';
|
|
3
|
+
export interface ActionOutcome {
|
|
4
|
+
ok: boolean;
|
|
5
|
+
message: string;
|
|
6
|
+
/** Canonical name the document answers to after the action — the new name
|
|
7
|
+
* after a move. Absent when the action changed no document. */
|
|
8
|
+
name?: string;
|
|
9
|
+
}
|
|
10
|
+
/** The notice a mutating action shows instead of writing, or null when the
|
|
11
|
+
* record is writable. The dossier marks a refusing record read-only so the
|
|
12
|
+
* refusal is never a surprise. */
|
|
13
|
+
export declare function readOnlyReason(record: DeliveryRecord, profileId: string | null): string | null;
|
|
14
|
+
/** Replace one document's whole surfaces list — refused when the list on disk
|
|
15
|
+
* holds more than the parsed list can carry back. */
|
|
16
|
+
export declare function writeSurfaces(record: DeliveryRecord, profileId: string | null, surfaces: SurfaceEntry[], success: string): Promise<ActionOutcome>;
|
|
17
|
+
/** The body exactly as it sits after the frontmatter fence, which is what the
|
|
18
|
+
* revision path saves back. The PARSED body eats the blank line the fence opens
|
|
19
|
+
* with, so seeding an editor from it would rewrite every doc it touched. */
|
|
20
|
+
export declare function readBody(record: DeliveryRecord): string;
|
|
21
|
+
/** Replace one document's whole body. */
|
|
22
|
+
export declare function writeBody(record: DeliveryRecord, profileId: string | null, body: string): Promise<ActionOutcome>;
|
|
23
|
+
/** Set or clear the document-level gate. An empty predicate clears it. */
|
|
24
|
+
export declare function writeGate(record: DeliveryRecord, profileId: string | null, gate: string): Promise<ActionOutcome>;
|
|
25
|
+
export declare function toggleFlag(record: DeliveryRecord, profileId: string | null, field: 'unlisted' | 'slash'): Promise<ActionOutcome>;
|
|
26
|
+
export declare function writeRoutingLine(record: DeliveryRecord, profileId: string | null, line: string): Promise<ActionOutcome>;
|
|
27
|
+
export declare function writeShortForm(record: DeliveryRecord, profileId: string | null, text: string): Promise<ActionOutcome>;
|
|
28
|
+
/** Rename or relocate a document inside its own store. */
|
|
29
|
+
export declare function moveDoc(record: DeliveryRecord, profileId: string | null, to: string): Promise<ActionOutcome>;
|
|
30
|
+
/** The profile project entry whose ceiling reaches a document's store, or null
|
|
31
|
+
* when the store carries the neutral ceiling because no entry covers it.
|
|
32
|
+
*
|
|
33
|
+
* Mirrors `resolveProjectScopeRoots`: pointers are walked in manifest order
|
|
34
|
+
* and the FIRST entry whose ancestor walk reaches the store's SCOPE root — the
|
|
35
|
+
* `.crouter/` dir one level above the `memory/` store — owns the ceiling, so
|
|
36
|
+
* cycling any other entry would move a number the store never reads. */
|
|
37
|
+
export declare function capOwnerFor(record: DeliveryRecord, profileId: string | null): {
|
|
38
|
+
path: string;
|
|
39
|
+
memory: Rung;
|
|
40
|
+
} | null;
|
|
41
|
+
export interface CapChange {
|
|
42
|
+
path: string;
|
|
43
|
+
memory: Rung;
|
|
44
|
+
}
|
|
45
|
+
/** Move a project relationship's automatic-delivery ceiling. */
|
|
46
|
+
export declare function writeCap(profileId: string, path: string, memory: Rung): ActionOutcome & {
|
|
47
|
+
change?: CapChange;
|
|
48
|
+
};
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
// Every write `crtr sys context admin` can make. Document changes go through
|
|
2
|
+
// the same `memory edit` / `memory move` leaves the CLI runs, always with
|
|
3
|
+
// rationale `editor`; a project relationship's memory ceiling goes through the
|
|
4
|
+
// profile manifest the profiles settings panel writes. Nothing here touches a
|
|
5
|
+
// memory file directly.
|
|
6
|
+
import { dirname } from 'node:path';
|
|
7
|
+
import { editLeaf } from '../../../memory/edit.js';
|
|
8
|
+
import { literalBodySegment } from '../../../memory/shared.js';
|
|
9
|
+
import { moveLeaf } from '../../../memory/move.js';
|
|
10
|
+
import { addProfileProject, loadProfileManifest } from '../../../../core/profiles/manifest.js';
|
|
11
|
+
import { findProjectScopeRoots } from '../../../../core/scope.js';
|
|
12
|
+
import { readText, realpathOrSelf } from '../../../../core/fs-utils.js';
|
|
13
|
+
import { parseSubstrateFrontmatter } from '../../../../core/substrate/schema.js';
|
|
14
|
+
import { surfaceEntryToFlag } from './model.js';
|
|
15
|
+
const RATIONALE = 'editor';
|
|
16
|
+
/** Which store a mutation must land in, or why the page cannot name one.
|
|
17
|
+
*
|
|
18
|
+
* The memory leaves resolve a bare name against the AMBIENT target — this
|
|
19
|
+
* process's cwd, profile and node — while the page plans against a dialled
|
|
20
|
+
* snapshot that may name a different one. Every mutation therefore carries an
|
|
21
|
+
* explicit selector for the store the record was actually read from. A record
|
|
22
|
+
* no selector reaches is READ-ONLY here rather than sent without one: the leaf
|
|
23
|
+
* would resolve the bare name ambiently and rewrite whatever wins it — a
|
|
24
|
+
* same-named override at a writable scope — and report that as success. */
|
|
25
|
+
function storeAddress(record, profileId) {
|
|
26
|
+
if (record.doc.plugin !== undefined) {
|
|
27
|
+
return {
|
|
28
|
+
refusal: `${record.name} belongs to the plugin “${record.doc.plugin}” and is read-only here — override it with \`crtr memory write ${record.name}\`.`,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if (record.scope === 'builtin') {
|
|
32
|
+
return {
|
|
33
|
+
refusal: `${record.name} is a builtin document and is read-only here — override it with \`crtr memory write ${record.name}\`.`,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
if (record.scope === 'project') {
|
|
37
|
+
const ownerDir = record.source.store.ownerDir;
|
|
38
|
+
return ownerDir === undefined
|
|
39
|
+
? { refusal: `${record.name} resolves from a project store with no owning directory, which this page cannot address — edit it with \`crtr memory edit ${record.name} --dir <dir>\`.` }
|
|
40
|
+
: { selector: { dir: ownerDir } };
|
|
41
|
+
}
|
|
42
|
+
if (record.scope === 'profile') {
|
|
43
|
+
return { selector: profileId === null ? { scope: 'profile' } : { scope: 'profile', profile: profileId } };
|
|
44
|
+
}
|
|
45
|
+
if (record.scope === 'user' || record.scope === 'node')
|
|
46
|
+
return { selector: { scope: record.scope } };
|
|
47
|
+
return { refusal: `${record.name} resolves from the ${record.scope} scope, which this page cannot address.` };
|
|
48
|
+
}
|
|
49
|
+
/** The notice a mutating action shows instead of writing, or null when the
|
|
50
|
+
* record is writable. The dossier marks a refusing record read-only so the
|
|
51
|
+
* refusal is never a surprise. */
|
|
52
|
+
export function readOnlyReason(record, profileId) {
|
|
53
|
+
const address = storeAddress(record, profileId);
|
|
54
|
+
return 'refusal' in address ? address.refusal : null;
|
|
55
|
+
}
|
|
56
|
+
function failure(error) {
|
|
57
|
+
return { ok: false, message: error instanceof Error ? error.message : String(error) };
|
|
58
|
+
}
|
|
59
|
+
async function runEdit(record, profileId, request, success) {
|
|
60
|
+
const address = storeAddress(record, profileId);
|
|
61
|
+
if ('refusal' in address)
|
|
62
|
+
return { ok: false, message: address.refusal };
|
|
63
|
+
const surfaces = request.surfaces;
|
|
64
|
+
const unset = [...(request.unset ?? [])];
|
|
65
|
+
if (surfaces !== undefined && surfaces.length === 0 && !unset.includes('surfaces'))
|
|
66
|
+
unset.push('surfaces');
|
|
67
|
+
try {
|
|
68
|
+
await editLeaf.run({
|
|
69
|
+
name: record.name,
|
|
70
|
+
rationale: RATIONALE,
|
|
71
|
+
...address.selector,
|
|
72
|
+
...(surfaces !== undefined && surfaces.length > 0 ? { surface: surfaces.map(surfaceEntryToFlag) } : {}),
|
|
73
|
+
...(request.body === undefined ? {} : { body: request.body }),
|
|
74
|
+
...(request.gate === undefined ? {} : { gate: request.gate }),
|
|
75
|
+
...(request.unlisted === undefined ? {} : { unlisted: true }),
|
|
76
|
+
...(request.slash === undefined ? {} : { slash: true }),
|
|
77
|
+
...(request.whenAndWhyToRead === undefined ? {} : { whenAndWhyToRead: request.whenAndWhyToRead }),
|
|
78
|
+
...(request.shortForm === undefined ? {} : { shortForm: request.shortForm }),
|
|
79
|
+
...(unset.length > 0 ? { unset } : {}),
|
|
80
|
+
});
|
|
81
|
+
return { ok: true, message: success, name: record.name };
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
return failure(error);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** Replace one document's whole surfaces list — refused when the list on disk
|
|
88
|
+
* holds more than the parsed list can carry back. */
|
|
89
|
+
export function writeSurfaces(record, profileId, surfaces, success) {
|
|
90
|
+
const refusal = readOnlyReason(record, profileId) ?? unwritableSurfaces(record);
|
|
91
|
+
if (refusal !== null)
|
|
92
|
+
return Promise.resolve({ ok: false, message: refusal });
|
|
93
|
+
return runEdit(record, profileId, { surfaces }, success);
|
|
94
|
+
}
|
|
95
|
+
/** Why the authored `surfaces` list cannot survive a write, or null when it can.
|
|
96
|
+
*
|
|
97
|
+
* A surface write REPLACES the whole list with the parsed entries, and the
|
|
98
|
+
* runtime parser is tolerant: it drops an entry it cannot read and strips a key
|
|
99
|
+
* it does not know. Writing that list back would delete authored content the
|
|
100
|
+
* page never showed, so an unrepresentable list refuses the edit instead —
|
|
101
|
+
* `memory edit --surface` admits only entries the strict coercion accepts, so
|
|
102
|
+
* carrying the original through verbatim is not available either. */
|
|
103
|
+
function unwritableSurfaces(record) {
|
|
104
|
+
const raw = record.source.frontmatter?.['surfaces'];
|
|
105
|
+
if (raw === undefined || raw === null)
|
|
106
|
+
return null;
|
|
107
|
+
const fix = `a write replaces the whole list, so fix ${record.storePath} first`;
|
|
108
|
+
if (!Array.isArray(raw)) {
|
|
109
|
+
return `The \`surfaces\` field of ${record.name} is not a list of entries — ${fix}.`;
|
|
110
|
+
}
|
|
111
|
+
for (const [index, entry] of raw.entries()) {
|
|
112
|
+
const loss = entryLoss(record.kind, entry);
|
|
113
|
+
if (loss !== null)
|
|
114
|
+
return `Entry ${index + 1} of ${record.name} ${loss} — ${fix}.`;
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
/** The frontmatter key behind each field a parsed entry carries back. */
|
|
119
|
+
const ENTRY_FIELDS = {
|
|
120
|
+
on: 'on',
|
|
121
|
+
at: 'at',
|
|
122
|
+
match: 'match',
|
|
123
|
+
'match-frontmatter': 'matchFrontmatter',
|
|
124
|
+
gate: 'gate',
|
|
125
|
+
};
|
|
126
|
+
function entryLoss(kind, raw) {
|
|
127
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
|
|
128
|
+
return 'is not an entry object';
|
|
129
|
+
const rec = raw;
|
|
130
|
+
const parsed = parseSubstrateFrontmatter({ kind, surfaces: [rec] })?.surfaces[0];
|
|
131
|
+
if (parsed === undefined)
|
|
132
|
+
return `(on ${JSON.stringify(rec['on'] ?? null)}) is malformed and the parser drops it`;
|
|
133
|
+
for (const key of Object.keys(rec)) {
|
|
134
|
+
if (rec[key] === undefined)
|
|
135
|
+
continue;
|
|
136
|
+
const field = ENTRY_FIELDS[key];
|
|
137
|
+
if (field === undefined)
|
|
138
|
+
return `carries an unknown key \`${key}\``;
|
|
139
|
+
if (parsed[field] === undefined)
|
|
140
|
+
return `carries a \`${key}\` the parser drops`;
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
/** The body exactly as it sits after the frontmatter fence, which is what the
|
|
145
|
+
* revision path saves back. The PARSED body eats the blank line the fence opens
|
|
146
|
+
* with, so seeding an editor from it would rewrite every doc it touched. */
|
|
147
|
+
export function readBody(record) {
|
|
148
|
+
const segment = literalBodySegment(readText(record.storePath));
|
|
149
|
+
return segment.startsWith('\n') ? segment.slice(1) : segment;
|
|
150
|
+
}
|
|
151
|
+
/** Replace one document's whole body. */
|
|
152
|
+
export function writeBody(record, profileId, body) {
|
|
153
|
+
if (body.trim() === '') {
|
|
154
|
+
return Promise.resolve({
|
|
155
|
+
ok: false,
|
|
156
|
+
message: `An empty body is not an edit \u2014 remove ${record.name} with \`crtr memory delete\` instead.`,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
return runEdit(record, profileId, { body }, `Rewrote the body of ${record.name}.`);
|
|
160
|
+
}
|
|
161
|
+
/** Set or clear the document-level gate. An empty predicate clears it. */
|
|
162
|
+
export function writeGate(record, profileId, gate) {
|
|
163
|
+
const trimmed = gate.trim();
|
|
164
|
+
return trimmed === ''
|
|
165
|
+
? runEdit(record, profileId, { unset: ['gate'] }, `Cleared the gate on ${record.name}.`)
|
|
166
|
+
: runEdit(record, profileId, { gate: trimmed }, `Gated ${record.name} on ${trimmed}.`);
|
|
167
|
+
}
|
|
168
|
+
export function toggleFlag(record, profileId, field) {
|
|
169
|
+
const on = field === 'unlisted' ? record.doc.unlisted : record.doc.slash;
|
|
170
|
+
if (on)
|
|
171
|
+
return runEdit(record, profileId, { unset: [field] }, `${record.name} is no longer ${field}.`);
|
|
172
|
+
const set = field === 'unlisted' ? { unlisted: true } : { slash: true };
|
|
173
|
+
return runEdit(record, profileId, set, `${record.name} is now ${field}.`);
|
|
174
|
+
}
|
|
175
|
+
export function writeRoutingLine(record, profileId, line) {
|
|
176
|
+
const trimmed = line.trim();
|
|
177
|
+
if (trimmed === '') {
|
|
178
|
+
return Promise.resolve({ ok: false, message: 'A routing line cannot be empty — it is the document\u2019s contract.' });
|
|
179
|
+
}
|
|
180
|
+
return runEdit(record, profileId, { whenAndWhyToRead: trimmed }, `Rewrote the routing line on ${record.name}.`);
|
|
181
|
+
}
|
|
182
|
+
export function writeShortForm(record, profileId, text) {
|
|
183
|
+
const trimmed = text.trim();
|
|
184
|
+
return trimmed === ''
|
|
185
|
+
? runEdit(record, profileId, { unset: ['short-form'] }, `Cleared the short form on ${record.name}.`)
|
|
186
|
+
: runEdit(record, profileId, { shortForm: trimmed }, `Rewrote the short form on ${record.name}.`);
|
|
187
|
+
}
|
|
188
|
+
/** Rename or relocate a document inside its own store. */
|
|
189
|
+
export async function moveDoc(record, profileId, to) {
|
|
190
|
+
const trimmed = to.trim();
|
|
191
|
+
if (trimmed === '')
|
|
192
|
+
return { ok: false, message: 'Type the new canonical name.' };
|
|
193
|
+
const address = storeAddress(record, profileId);
|
|
194
|
+
if ('refusal' in address)
|
|
195
|
+
return { ok: false, message: address.refusal };
|
|
196
|
+
try {
|
|
197
|
+
const result = (await moveLeaf.run({
|
|
198
|
+
name: record.name,
|
|
199
|
+
to: trimmed,
|
|
200
|
+
...address.selector,
|
|
201
|
+
}));
|
|
202
|
+
const name = typeof result?.['name'] === 'string' ? result['name'] : trimmed;
|
|
203
|
+
return { ok: true, message: `${record.name} now answers to ${name}.`, name };
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
return failure(error);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/** The profile project entry whose ceiling reaches a document's store, or null
|
|
210
|
+
* when the store carries the neutral ceiling because no entry covers it.
|
|
211
|
+
*
|
|
212
|
+
* Mirrors `resolveProjectScopeRoots`: pointers are walked in manifest order
|
|
213
|
+
* and the FIRST entry whose ancestor walk reaches the store's SCOPE root — the
|
|
214
|
+
* `.crouter/` dir one level above the `memory/` store — owns the ceiling, so
|
|
215
|
+
* cycling any other entry would move a number the store never reads. */
|
|
216
|
+
export function capOwnerFor(record, profileId) {
|
|
217
|
+
if (profileId === null || record.scope !== 'project')
|
|
218
|
+
return null;
|
|
219
|
+
// Realpathed on both sides: a manifest path is stored already resolved while
|
|
220
|
+
// a store root is derived from the dialled cwd, so a symlinked ancestor would
|
|
221
|
+
// otherwise read as no entry governing the store.
|
|
222
|
+
const scopeRoot = realpathOrSelf(dirname(record.source.store.storeRoot));
|
|
223
|
+
let manifest;
|
|
224
|
+
try {
|
|
225
|
+
manifest = loadProfileManifest(profileId).manifest;
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
for (const project of manifest.projects) {
|
|
231
|
+
if (findProjectScopeRoots(project.path, null).some((root) => realpathOrSelf(root) === scopeRoot)) {
|
|
232
|
+
return { path: project.path, memory: project.memory };
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
/** Move a project relationship's automatic-delivery ceiling. */
|
|
238
|
+
export function writeCap(profileId, path, memory) {
|
|
239
|
+
try {
|
|
240
|
+
addProfileProject(profileId, path, { memory });
|
|
241
|
+
return { ok: true, message: `${path} discloses up to "${memory}".`, change: { path, memory } };
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
return failure(error);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type Snapshot } from './model.js';
|
|
2
|
+
export type AxisId = 'event' | 'payload' | 'kind' | 'mode' | 'lifecycle' | 'manager' | 'depth' | 'profile' | 'cwd';
|
|
3
|
+
interface Axis {
|
|
4
|
+
id: AxisId;
|
|
5
|
+
label: string;
|
|
6
|
+
value: string;
|
|
7
|
+
/** Free-text axes open a field; the rest cycle in place. */
|
|
8
|
+
typed: boolean;
|
|
9
|
+
}
|
|
10
|
+
/** The rail's rows for a snapshot. The payload row appears only for the events
|
|
11
|
+
* that read one, because on boot there is nothing it could change. */
|
|
12
|
+
export declare function axesOf(snapshot: Snapshot, profileName: string): Axis[];
|
|
13
|
+
/** Cycle one enum axis, returning the changed snapshot. A typed axis is never
|
|
14
|
+
* cycled — the shell opens a field for it instead. */
|
|
15
|
+
export declare function cycleAxis(snapshot: Snapshot, id: AxisId, delta: number): Snapshot;
|
|
16
|
+
/** Apply a typed axis's submitted value. */
|
|
17
|
+
export declare function setTypedAxis(snapshot: Snapshot, id: AxisId, value: string): Snapshot;
|
|
18
|
+
/** The value a typed axis's field is seeded with. */
|
|
19
|
+
export declare function typedAxisValue(snapshot: Snapshot, id: AxisId): string;
|
|
20
|
+
export declare function renderAxes(axes: Axis[], index: number, width: number, height: number, focused: boolean): string[];
|
|
21
|
+
export {};
|