@crouton-kit/crouter 0.3.17 → 0.3.19
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/build-root.js +6 -0
- package/dist/builtin-memory/crouter-development/marketplaces.md +164 -0
- package/dist/builtin-memory/crouter-development/personas/base-prompt.md +58 -0
- package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +60 -0
- package/dist/builtin-memory/crouter-development/personas.md +117 -0
- package/dist/builtin-memory/crouter-development/plugins.md +163 -0
- package/dist/builtin-memory/design.md +60 -0
- package/dist/builtin-memory/development.md +116 -0
- package/dist/builtin-memory/planning.md +66 -0
- package/dist/builtin-memory/spec.md +99 -0
- package/dist/builtin-personas/design/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/design/orchestrator.md +1 -1
- package/dist/builtin-personas/developer/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/developer/orchestrator.md +1 -1
- package/dist/builtin-personas/explore/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/general/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/lifecycle/resident.md +1 -1
- package/dist/builtin-personas/lifecycle/terminal.md +5 -2
- package/dist/builtin-personas/orchestration-kernel.md +15 -24
- package/dist/builtin-personas/plan/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/plan/orchestrator.md +1 -1
- package/dist/builtin-personas/plan/reviewers/architecture-fit/{base.md → PERSONA.md} +1 -1
- package/dist/builtin-personas/plan/reviewers/code-smells/{base.md → PERSONA.md} +1 -1
- package/dist/builtin-personas/plan/reviewers/pattern-consistency/{base.md → PERSONA.md} +1 -1
- package/dist/builtin-personas/plan/reviewers/requirements-coverage/{base.md → PERSONA.md} +1 -1
- package/dist/builtin-personas/plan/reviewers/security/{base.md → PERSONA.md} +1 -1
- package/dist/builtin-personas/review/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/runtime-base.md +6 -3
- package/dist/builtin-personas/spec/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/spec/orchestrator.md +1 -1
- package/dist/builtin-personas/waiting.md +8 -0
- package/dist/builtin-skills/skills/crouter-development/personas/SKILL.md +24 -14
- package/dist/builtin-skills/skills/crouter-development/personas/base-prompt/SKILL.md +4 -4
- package/dist/builtin-skills/skills/crouter-development/personas/orchestrator-prompt/SKILL.md +1 -1
- package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +2 -2
- package/dist/builtin-views/_lib/states.mjs +161 -0
- package/dist/builtin-views/canvas/client.mjs +303 -0
- package/dist/builtin-views/canvas/view.mjs +576 -0
- package/dist/builtin-views/git-pr/client.mjs +440 -0
- package/dist/builtin-views/git-pr/view.mjs +675 -0
- package/dist/builtin-views/inbox/_lib/render.mjs +177 -0
- package/dist/builtin-views/inbox/sources/gmail.mjs +965 -0
- package/dist/builtin-views/inbox/sources/linkedin.mjs +427 -0
- package/dist/builtin-views/inbox/view.mjs +889 -0
- package/dist/builtin-views/linkedin/client.mjs +610 -0
- package/dist/builtin-views/linkedin/view.mjs +1171 -0
- package/dist/clients/attach/attach-cmd.d.ts +2 -0
- package/dist/clients/attach/attach-cmd.js +354 -0
- package/dist/clients/attach/chat-view.d.ts +77 -0
- package/dist/clients/attach/chat-view.js +450 -0
- package/dist/clients/attach/clipboard-image.d.ts +16 -0
- package/dist/clients/attach/clipboard-image.js +113 -0
- package/dist/clients/attach/config-load.d.ts +31 -0
- package/dist/clients/attach/config-load.js +113 -0
- package/dist/clients/attach/extension-dialogs.d.ts +29 -0
- package/dist/clients/attach/extension-dialogs.js +101 -0
- package/dist/clients/attach/input-controller.d.ts +54 -0
- package/dist/clients/attach/input-controller.js +204 -0
- package/dist/clients/attach/slash-commands.d.ts +36 -0
- package/dist/clients/attach/slash-commands.js +200 -0
- package/dist/clients/attach/view-socket.d.ts +48 -0
- package/dist/clients/attach/view-socket.js +126 -0
- package/dist/commands/attention.js +3 -3
- package/dist/commands/canvas-prune.js +1 -1
- package/dist/commands/daemon.js +4 -3
- package/dist/commands/human/prompts.js +4 -10
- package/dist/commands/human/queue.js +43 -8
- package/dist/commands/human/shared.d.ts +28 -1
- package/dist/commands/human/shared.js +48 -10
- package/dist/commands/memory/find.d.ts +1 -0
- package/dist/commands/memory/find.js +180 -0
- package/dist/commands/memory/lint.d.ts +1 -0
- package/dist/commands/memory/lint.js +140 -0
- package/dist/commands/memory/list.d.ts +1 -0
- package/dist/commands/memory/list.js +79 -0
- package/dist/commands/memory/read.js +103 -0
- package/dist/commands/memory/shared.d.ts +30 -0
- package/dist/commands/memory/shared.js +122 -0
- package/dist/commands/memory/write.d.ts +1 -0
- package/dist/commands/memory/write.js +85 -0
- package/dist/commands/memory.d.ts +2 -0
- package/dist/commands/memory.js +27 -0
- package/dist/commands/node.d.ts +3 -2
- package/dist/commands/node.js +660 -65
- package/dist/commands/pkg/market-manage.js +1 -1
- package/dist/commands/push.js +6 -6
- package/dist/commands/revive.js +1 -1
- package/dist/commands/skill/author.js +1 -1
- package/dist/commands/skill/shared.d.ts +1 -8
- package/dist/commands/skill/shared.js +2 -55
- package/dist/commands/skill.js +9 -14
- package/dist/commands/sys/doctor.js +1 -1
- package/dist/commands/sys/update.js +1 -1
- package/dist/commands/view-cycle.d.ts +2 -0
- package/dist/commands/view-cycle.js +125 -0
- package/dist/commands/view-list.d.ts +2 -0
- package/dist/commands/view-list.js +66 -0
- package/dist/commands/view-new.d.ts +2 -0
- package/dist/commands/view-new.js +70 -0
- package/dist/commands/view-pick.d.ts +2 -0
- package/dist/commands/view-pick.js +119 -0
- package/dist/commands/view-run.d.ts +2 -0
- package/dist/commands/view-run.js +191 -0
- package/dist/commands/view.d.ts +2 -0
- package/dist/commands/view.js +29 -0
- package/dist/core/__tests__/broker-lifecycle.test.d.ts +1 -0
- package/dist/core/__tests__/broker-lifecycle.test.js +677 -0
- package/dist/core/__tests__/broker-sdk-wiring.test.d.ts +1 -0
- package/dist/core/__tests__/broker-sdk-wiring.test.js +166 -0
- package/dist/core/__tests__/cascade-close.test.js +12 -2
- package/dist/core/__tests__/child-death-wake.test.d.ts +1 -0
- package/dist/core/__tests__/child-death-wake.test.js +245 -0
- package/dist/core/__tests__/context-intro.test.js +76 -62
- package/dist/core/__tests__/daemon-boot.test.js +14 -5
- package/dist/core/__tests__/daemon-liveness.test.js +34 -9
- package/dist/core/__tests__/detach-focus.test.d.ts +1 -0
- package/dist/core/__tests__/detach-focus.test.js +206 -0
- package/dist/core/__tests__/draw-style.test.d.ts +1 -0
- package/dist/core/__tests__/draw-style.test.js +258 -0
- package/dist/core/__tests__/fixtures/c3-custom-provider-ext.d.ts +2 -0
- package/dist/core/__tests__/fixtures/c3-custom-provider-ext.js +18 -0
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +138 -0
- package/dist/core/__tests__/fixtures/fake-engine.js +614 -0
- package/dist/core/__tests__/fixtures/fake-pi-host.js +1 -0
- package/dist/core/__tests__/flagship-lifecycle.test.js +7 -1
- package/dist/core/__tests__/frame-decoder-perf.test.d.ts +1 -0
- package/dist/core/__tests__/frame-decoder-perf.test.js +198 -0
- package/dist/core/__tests__/helpers/harness.d.ts +9 -0
- package/dist/core/__tests__/helpers/harness.js +111 -1
- package/dist/core/__tests__/home-session.test.js +41 -8
- package/dist/core/__tests__/human-new-window-regression.test.d.ts +1 -0
- package/dist/core/__tests__/human-new-window-regression.test.js +101 -0
- package/dist/core/__tests__/human-surface-target.test.d.ts +1 -0
- package/dist/core/__tests__/human-surface-target.test.js +98 -0
- package/dist/core/__tests__/kickoff.test.js +37 -3
- package/dist/core/__tests__/live-mutation.test.js +50 -33
- package/dist/core/__tests__/memory.test.js +23 -115
- package/dist/core/__tests__/persona-subkind.test.js +18 -15
- package/dist/core/__tests__/placement-focus.test.js +5 -0
- package/dist/core/__tests__/placement-teardown.test.js +54 -11
- package/dist/core/__tests__/relaunch.test.js +4 -3
- package/dist/core/__tests__/review-render-pane-regression.test.d.ts +1 -0
- package/dist/core/__tests__/review-render-pane-regression.test.js +133 -0
- package/dist/core/__tests__/spawn-root.test.js +10 -0
- package/dist/core/__tests__/spike-harness.test.js +1 -0
- package/dist/core/__tests__/wake-bearings.test.d.ts +1 -0
- package/dist/core/__tests__/wake-bearings.test.js +156 -0
- package/dist/core/__tests__/wake-origin.test.d.ts +1 -0
- package/dist/core/__tests__/wake-origin.test.js +110 -0
- package/dist/core/bootstrap.js +1 -1
- package/dist/core/canvas/browse/__tests__/render.test.js +1 -0
- package/dist/core/canvas/browse/app.js +24 -2
- package/dist/core/canvas/browse/model.d.ts +38 -2
- package/dist/core/canvas/browse/model.js +134 -10
- package/dist/core/canvas/browse/render.d.ts +6 -12
- package/dist/core/canvas/browse/render.js +72 -104
- package/dist/core/canvas/canvas.js +15 -22
- package/dist/core/canvas/db.js +46 -0
- package/dist/core/canvas/index.d.ts +1 -0
- package/dist/core/canvas/index.js +1 -0
- package/dist/core/canvas/paths.d.ts +4 -1
- package/dist/core/canvas/paths.js +10 -4
- package/dist/core/canvas/pid.d.ts +4 -0
- package/dist/core/canvas/pid.js +23 -0
- package/dist/core/canvas/render.d.ts +11 -2
- package/dist/core/canvas/render.js +69 -0
- package/dist/core/canvas/types.d.ts +85 -3
- package/dist/core/canvas/types.js +2 -2
- package/dist/core/canvas/wakeups.d.ts +76 -0
- package/dist/core/canvas/wakeups.js +185 -0
- package/dist/core/config.js +4 -1
- package/dist/core/frontmatter.js +37 -124
- package/dist/core/help.d.ts +6 -0
- package/dist/core/help.js +7 -0
- package/dist/core/memory-resolver.d.ts +49 -0
- package/dist/core/memory-resolver.js +141 -0
- package/dist/core/personas/index.d.ts +4 -3
- package/dist/core/personas/index.js +3 -2
- package/dist/core/personas/loader.d.ts +41 -16
- package/dist/core/personas/loader.js +133 -29
- package/dist/core/personas/resolve.d.ts +4 -4
- package/dist/core/personas/resolve.js +28 -16
- package/dist/core/predicate.d.ts +63 -0
- package/dist/core/predicate.js +189 -0
- package/dist/core/resolver.js +26 -5
- package/dist/core/runtime/bearings.d.ts +53 -12
- package/dist/core/runtime/bearings.js +132 -59
- package/dist/core/runtime/broker-cli.d.ts +1 -0
- package/dist/core/runtime/broker-cli.js +46 -0
- package/dist/core/runtime/broker-protocol.d.ts +332 -0
- package/dist/core/runtime/broker-protocol.js +153 -0
- package/dist/core/runtime/broker-sdk.d.ts +48 -0
- package/dist/core/runtime/broker-sdk.js +72 -0
- package/dist/core/runtime/broker.d.ts +55 -0
- package/dist/core/runtime/broker.js +1128 -0
- package/dist/core/runtime/close.js +35 -6
- package/dist/core/runtime/host.d.ts +53 -0
- package/dist/core/runtime/host.js +186 -0
- package/dist/core/runtime/kickoff.d.ts +2 -1
- package/dist/core/runtime/kickoff.js +91 -5
- package/dist/core/runtime/launch.d.ts +45 -2
- package/dist/core/runtime/launch.js +65 -2
- package/dist/core/runtime/lifecycle.js +23 -6
- package/dist/core/runtime/memory.d.ts +2 -42
- package/dist/core/runtime/memory.js +11 -162
- package/dist/core/runtime/nodes.d.ts +33 -0
- package/dist/core/runtime/nodes.js +59 -1
- package/dist/core/runtime/persona.js +21 -11
- package/dist/core/runtime/pi-vendored.d.ts +18 -0
- package/dist/core/runtime/pi-vendored.js +49 -0
- package/dist/core/runtime/placement.d.ts +42 -14
- package/dist/core/runtime/placement.js +228 -38
- package/dist/core/runtime/promote.d.ts +0 -6
- package/dist/core/runtime/promote.js +1 -12
- package/dist/core/runtime/{demote.d.ts → recycle.d.ts} +5 -5
- package/dist/core/runtime/{demote.js → recycle.js} +27 -11
- package/dist/core/runtime/reset.js +8 -6
- package/dist/core/runtime/revive.d.ts +2 -0
- package/dist/core/runtime/revive.js +34 -34
- package/dist/core/runtime/spawn.d.ts +19 -0
- package/dist/core/runtime/spawn.js +75 -22
- package/dist/core/runtime/stop-guard.d.ts +1 -1
- package/dist/core/runtime/stop-guard.js +6 -1
- package/dist/core/runtime/tmux-chrome.d.ts +1 -1
- package/dist/core/runtime/tmux-chrome.js +1 -1
- package/dist/core/runtime/tmux.d.ts +28 -0
- package/dist/core/runtime/tmux.js +80 -6
- package/dist/core/scope.d.ts +11 -0
- package/dist/core/scope.js +39 -0
- package/dist/core/spawn.d.ts +25 -1
- package/dist/core/spawn.js +72 -7
- package/dist/core/substrate/gate.d.ts +13 -0
- package/dist/core/substrate/gate.js +21 -0
- package/dist/core/substrate/index.d.ts +7 -0
- package/dist/core/substrate/index.js +18 -0
- package/dist/core/substrate/on-read.d.ts +14 -0
- package/dist/core/substrate/on-read.js +292 -0
- package/dist/core/substrate/render.d.ts +25 -0
- package/dist/core/substrate/render.js +256 -0
- package/dist/core/substrate/schema.d.ts +76 -0
- package/dist/core/substrate/schema.js +124 -0
- package/dist/core/substrate/session-cache.d.ts +30 -0
- package/dist/core/substrate/session-cache.js +77 -0
- package/dist/core/substrate/subject.d.ts +41 -0
- package/dist/core/substrate/subject.js +54 -0
- package/dist/core/tui/contract.d.ts +83 -0
- package/dist/core/tui/contract.js +8 -0
- package/dist/core/tui/draw.d.ts +96 -0
- package/dist/core/tui/draw.js +339 -0
- package/dist/core/tui/host.d.ts +29 -0
- package/dist/core/tui/host.js +379 -0
- package/dist/core/tui/loader.d.ts +16 -0
- package/dist/core/tui/loader.js +94 -0
- package/dist/core/{canvas/browse → tui}/terminal.js +7 -6
- package/dist/core/wake.d.ts +86 -0
- package/dist/core/wake.js +308 -0
- package/dist/daemon/crtrd.d.ts +29 -4
- package/dist/daemon/crtrd.js +662 -46
- package/dist/pi-extensions/__tests__/canvas-context-intro.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +171 -0
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +19 -12
- package/dist/pi-extensions/canvas-commands.d.ts +3 -0
- package/dist/pi-extensions/canvas-commands.js +10 -0
- package/dist/pi-extensions/canvas-context-intro.d.ts +17 -0
- package/dist/pi-extensions/canvas-context-intro.js +55 -15
- package/dist/pi-extensions/canvas-doc-substrate.d.ts +44 -0
- package/dist/pi-extensions/canvas-doc-substrate.js +112 -0
- package/dist/pi-extensions/canvas-nav.d.ts +3 -0
- package/dist/pi-extensions/canvas-nav.js +145 -34
- package/dist/pi-extensions/canvas-stophook.js +17 -8
- package/dist/pi-extensions/canvas-view.d.ts +21 -0
- package/dist/pi-extensions/canvas-view.js +75 -0
- package/dist/prompts/skill.js +19 -26
- package/dist/prompts/view.d.ts +7 -0
- package/dist/prompts/view.js +101 -0
- package/dist/types.d.ts +4 -0
- package/dist/types.js +1 -0
- package/package.json +9 -4
- package/dist/commands/skill/find.d.ts +0 -4
- package/dist/commands/skill/find.js +0 -257
- package/dist/commands/skill/read.js +0 -91
- /package/dist/commands/{skill → memory}/read.d.ts +0 -0
- /package/dist/core/{canvas/browse → tui}/terminal.d.ts +0 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// wakeups.ts — the WAKEUPS table data-access layer (canvas.db, migration v7).
|
|
2
|
+
//
|
|
3
|
+
// One-module-per-table (the focuses.ts precedent): ALL SQL that touches the
|
|
4
|
+
// `wakeups` table lives HERE and nowhere else — never appended to canvas.ts,
|
|
5
|
+
// which keeps the node/edge model. A WAKEUP is one durable row = "do <kind> at
|
|
6
|
+
// fire_at [every recur]", carrying a fire time, a kind, an optional payload, an
|
|
7
|
+
// optional recurrence cadence, a nullable TARGET node_id, and the owner_id of
|
|
8
|
+
// the node that ARMED it.
|
|
9
|
+
//
|
|
10
|
+
// Two anchors, two reap rules (design D1/D2):
|
|
11
|
+
// - node_id → the revive target AND the node-anchored cancel anchor; it
|
|
12
|
+
// carries the `ON DELETE CASCADE` FK, so a pruned node's node-anchored wakes
|
|
13
|
+
// are reaped FOR FREE in pruneNodes' same `DELETE FROM nodes` (canvas.ts is
|
|
14
|
+
// UNTOUCHED — the cascade does it; there is no pruneNodes wakeups code).
|
|
15
|
+
// NULL = canvas-detached (a deferred spawn / spawn-cron).
|
|
16
|
+
// - owner_id → the armer; a PLAIN indexed column with NO FK, so a crashed-
|
|
17
|
+
// then-pruned armer's detached cron is NOT cascade-dropped (Invariant E).
|
|
18
|
+
// Reaped only by the explicit cancelWakesFor DELETE on deliberate close.
|
|
19
|
+
//
|
|
20
|
+
// Each function is a single-statement query mirroring the canvas.ts atomic-setter
|
|
21
|
+
// pattern (setStatus/setIntent/setPresence) — no read-modify-write, serialized by
|
|
22
|
+
// WAL. The data-access layer OWNS payload serialization: armWake JSON.stringifies
|
|
23
|
+
// the typed per-kind union on write; dueWakes/listWakes JSON.parse it back on read
|
|
24
|
+
// (Maj-1), so callers consume `payload.body` / `payload as SpawnChildOpts` directly.
|
|
25
|
+
//
|
|
26
|
+
// `wakeup_id` is CALLER-SUPPLIED (the command surface mints `wk-${newNodeId()}`):
|
|
27
|
+
// the canvas layer never imports newNodeId from runtime/nodes.ts — that would
|
|
28
|
+
// force a canvas → runtime → canvas import cycle (Min-1).
|
|
29
|
+
import { openDb } from './db.js';
|
|
30
|
+
/** A thrown integrity-backstop error from armWake. `code` is the AC-N3 code the
|
|
31
|
+
* command surface (the ONLY sanctioned caller of armWake) maps to a rendered
|
|
32
|
+
* error block — armWake carries integrity backstops only (empty body / recur-on-
|
|
33
|
+
* deadline / unknown kind); target-resolvability + recoverable-state + the
|
|
34
|
+
* per-owner cap live in the surface (Min-6), so a non-surface armer could insert
|
|
35
|
+
* a dangling wake — every armer MUST route through the surface. */
|
|
36
|
+
export class WakeArmError extends Error {
|
|
37
|
+
code;
|
|
38
|
+
constructor(code, message) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.code = code;
|
|
41
|
+
this.name = 'WakeArmError';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** Hydrate a raw row into a Wakeup, JSON.parsing `payload` back into the typed
|
|
45
|
+
* per-kind union (NULL column → null). */
|
|
46
|
+
function wakeupFrom(r) {
|
|
47
|
+
const raw = r['payload'];
|
|
48
|
+
return {
|
|
49
|
+
wakeup_id: r['wakeup_id'],
|
|
50
|
+
node_id: r['node_id'] ?? null,
|
|
51
|
+
owner_id: r['owner_id'],
|
|
52
|
+
fire_at: r['fire_at'],
|
|
53
|
+
kind: r['kind'],
|
|
54
|
+
recur: r['recur'] ?? null,
|
|
55
|
+
payload: raw == null ? null : JSON.parse(raw),
|
|
56
|
+
created: r['created'],
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// Writes — each a single-statement INSERT/UPDATE/DELETE.
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
/** INSERT one wakeup row from the pinned spec, returning its (caller-supplied)
|
|
63
|
+
* `wakeup_id`. JSON.stringifies the typed `payload` union into the TEXT column
|
|
64
|
+
* and stamps `created`.
|
|
65
|
+
*
|
|
66
|
+
* Deadline upsert (design Q3, "≤1 deadline per node"): for `kind === 'deadline'`
|
|
67
|
+
* the node's existing deadline is canceled first, then the new one inserted — the
|
|
68
|
+
* partial unique index `idx_wakeups_deadline` is the schema-level backstop.
|
|
69
|
+
*
|
|
70
|
+
* Integrity backstops (throw WakeArmError; the surface maps the code to AC-N3):
|
|
71
|
+
* - empty/whitespace note `body` for `noted`/`deadline` → `empty_note`.
|
|
72
|
+
* - non-NULL `recur` on a `deadline` → `deadline_cannot_recur`.
|
|
73
|
+
* - unknown `kind` → `bad_kind`.
|
|
74
|
+
* NO target-resolvability / recoverable-state / cap backstop here (Min-6). */
|
|
75
|
+
export function armWake(spec) {
|
|
76
|
+
const { kind } = spec;
|
|
77
|
+
if (kind !== 'bare' && kind !== 'noted' && kind !== 'deadline' && kind !== 'spawn') {
|
|
78
|
+
throw new WakeArmError('bad_kind', `unknown wake kind: ${String(kind)}`);
|
|
79
|
+
}
|
|
80
|
+
if (kind === 'noted' || kind === 'deadline') {
|
|
81
|
+
const body = spec.payload?.body;
|
|
82
|
+
if (typeof body !== 'string' || body.trim() === '') {
|
|
83
|
+
throw new WakeArmError('empty_note', `a ${kind} wake requires a non-empty note body.`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (kind === 'deadline' && spec.recur != null) {
|
|
87
|
+
throw new WakeArmError('deadline_cannot_recur', 'a deadline wake cannot recur.');
|
|
88
|
+
}
|
|
89
|
+
// Deadline upsert: clear the node's existing deadline before inserting the new one.
|
|
90
|
+
if (kind === 'deadline' && spec.node_id != null) {
|
|
91
|
+
cancelDeadlinesFor(spec.node_id);
|
|
92
|
+
}
|
|
93
|
+
const payloadText = spec.payload == null ? null : JSON.stringify(spec.payload);
|
|
94
|
+
openDb()
|
|
95
|
+
.prepare(`INSERT INTO wakeups (wakeup_id, node_id, owner_id, fire_at, kind, recur, payload, created)
|
|
96
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
97
|
+
.run(spec.wakeup_id, spec.node_id ?? null, spec.owner_id, spec.fire_at, kind, spec.recur ?? null, payloadText, new Date().toISOString());
|
|
98
|
+
return spec.wakeup_id;
|
|
99
|
+
}
|
|
100
|
+
/** UPDATE the next fire time — recurrence advancement (the daemon settles a
|
|
101
|
+
* recurring row to its next slot BEFORE enacting, crash-safe; design D4). */
|
|
102
|
+
export function advanceWake(wakeup_id, nextFireIso) {
|
|
103
|
+
openDb().prepare('UPDATE wakeups SET fire_at = ? WHERE wakeup_id = ?').run(nextFireIso, wakeup_id);
|
|
104
|
+
}
|
|
105
|
+
/** DELETE one wakeup by id — one-shot consumption (the daemon settles a one-shot
|
|
106
|
+
* row BEFORE enacting, crash-safe; design D4). */
|
|
107
|
+
export function consumeWake(wakeup_id) {
|
|
108
|
+
openDb().prepare('DELETE FROM wakeups WHERE wakeup_id = ?').run(wakeup_id);
|
|
109
|
+
}
|
|
110
|
+
/** DELETE one wakeup by id — the explicit user/surface cancel. Idempotent:
|
|
111
|
+
* 0 rows ⇒ no error (AC-C2). */
|
|
112
|
+
export function cancelWake(wakeup_id) {
|
|
113
|
+
openDb().prepare('DELETE FROM wakeups WHERE wakeup_id = ?').run(wakeup_id);
|
|
114
|
+
}
|
|
115
|
+
/** DELETE every wake anchored to this node AND every detached wake it ARMED —
|
|
116
|
+
* deliberate close/reap (ruling A). Hooked at transition('cancel'). */
|
|
117
|
+
export function cancelWakesFor(node_id) {
|
|
118
|
+
openDb()
|
|
119
|
+
.prepare('DELETE FROM wakeups WHERE node_id = ? OR owner_id = ?')
|
|
120
|
+
.run(node_id, node_id);
|
|
121
|
+
}
|
|
122
|
+
/** DELETE this node's one-shot self-alarms (recur IS NULL); node-anchored crons
|
|
123
|
+
* SURVIVE — finalize is the instance finishing, not a teardown of standing
|
|
124
|
+
* schedules (design Q1). Hooked at transition('finalize'). */
|
|
125
|
+
export function cancelSelfAlarms(node_id) {
|
|
126
|
+
openDb().prepare('DELETE FROM wakeups WHERE node_id = ? AND recur IS NULL').run(node_id);
|
|
127
|
+
}
|
|
128
|
+
/** DELETE this node's pending deadline(s) — cancel-on-wake (design §6.4).
|
|
129
|
+
* Hooked at reviveNode, so the deadline always belongs to the dormancy left. */
|
|
130
|
+
export function cancelDeadlinesFor(node_id) {
|
|
131
|
+
openDb().prepare("DELETE FROM wakeups WHERE node_id = ? AND kind = 'deadline'").run(node_id);
|
|
132
|
+
}
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
// Reads.
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
/** The daemon's per-tick due query: every row whose fire_at has arrived, in
|
|
137
|
+
* fire_at order, with `payload` parsed into the typed union. Returns exactly ONE
|
|
138
|
+
* row per recurring wake regardless of missed slots — coalescing is structural
|
|
139
|
+
* (AC-E2). */
|
|
140
|
+
export function dueWakes(nowIso) {
|
|
141
|
+
return openDb()
|
|
142
|
+
.prepare('SELECT * FROM wakeups WHERE fire_at <= ? ORDER BY fire_at')
|
|
143
|
+
.all(nowIso).map(wakeupFrom);
|
|
144
|
+
}
|
|
145
|
+
/** List wakes for a scope (node / owner / subtree / due / canvas), `payload`
|
|
146
|
+
* parsed as in dueWakes. The subtree variant binds the id set with `?`
|
|
147
|
+
* placeholders (mirroring listNodes — the codebase's one safe dynamic-IN spot);
|
|
148
|
+
* ids are NEVER string-concatenated (Min-10). */
|
|
149
|
+
export function listWakes(scope) {
|
|
150
|
+
const db = openDb();
|
|
151
|
+
let rows;
|
|
152
|
+
if ('node' in scope) {
|
|
153
|
+
rows = db
|
|
154
|
+
.prepare('SELECT * FROM wakeups WHERE node_id = ? ORDER BY fire_at')
|
|
155
|
+
.all(scope.node);
|
|
156
|
+
}
|
|
157
|
+
else if ('owner' in scope) {
|
|
158
|
+
rows = db
|
|
159
|
+
.prepare('SELECT * FROM wakeups WHERE owner_id = ? ORDER BY fire_at')
|
|
160
|
+
.all(scope.owner);
|
|
161
|
+
}
|
|
162
|
+
else if ('subtree' in scope) {
|
|
163
|
+
if (scope.subtree.length === 0)
|
|
164
|
+
return [];
|
|
165
|
+
const placeholders = scope.subtree.map(() => '?').join(',');
|
|
166
|
+
rows = db
|
|
167
|
+
.prepare(`SELECT * FROM wakeups WHERE node_id IN (${placeholders}) ORDER BY fire_at`)
|
|
168
|
+
.all(...scope.subtree);
|
|
169
|
+
}
|
|
170
|
+
else if ('due' in scope) {
|
|
171
|
+
rows = db
|
|
172
|
+
.prepare('SELECT * FROM wakeups WHERE fire_at <= ? ORDER BY fire_at')
|
|
173
|
+
.all(scope.due);
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
rows = db.prepare('SELECT * FROM wakeups ORDER BY fire_at').all();
|
|
177
|
+
}
|
|
178
|
+
return rows.map(wakeupFrom);
|
|
179
|
+
}
|
|
180
|
+
/** True if the node has ANY pending self-anchored wake (of any kind). Consumed by
|
|
181
|
+
* the stop-guard self-wake seam so a no-child poll node releases dormant after
|
|
182
|
+
* arming instead of being nagged to finish (AC-X3/AC-R1). */
|
|
183
|
+
export function hasPendingSelfWake(node_id) {
|
|
184
|
+
return (openDb().prepare('SELECT 1 FROM wakeups WHERE node_id = ? LIMIT 1').get(node_id) !== undefined);
|
|
185
|
+
}
|
package/dist/core/config.js
CHANGED
|
@@ -137,7 +137,10 @@ function mergeConfig(partial) {
|
|
|
137
137
|
? Math.floor(rawMaxPanes)
|
|
138
138
|
: defaults.max_panes_per_window;
|
|
139
139
|
const canvasNav = mergeCanvasNav(partial.canvasNav);
|
|
140
|
-
|
|
140
|
+
// The merge drops unknown keys, so `headless` must be carried explicitly or it
|
|
141
|
+
// is lost on every read-modify-write of config.json.
|
|
142
|
+
const headless = typeof partial.headless === 'boolean' ? partial.headless : defaults.headless;
|
|
143
|
+
return { schema_version, marketplaces, plugins, skills, auto_update, max_panes_per_window, canvasNav, headless };
|
|
141
144
|
}
|
|
142
145
|
export function updateConfig(scope, mutate) {
|
|
143
146
|
const cfg = readConfig(scope);
|
package/dist/core/frontmatter.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parse as parseYaml } from 'yaml';
|
|
1
2
|
import { isSkillType } from '../types.js';
|
|
2
3
|
const FRONTMATTER_RE = /^---\s*\r?\n([\s\S]*?)\r?\n---\s*\r?\n?/;
|
|
3
4
|
export function parseFrontmatter(source) {
|
|
@@ -7,7 +8,7 @@ export function parseFrontmatter(source) {
|
|
|
7
8
|
}
|
|
8
9
|
const raw = match[1];
|
|
9
10
|
const body = source.slice(match[0].length);
|
|
10
|
-
return { data: toSkillFrontmatter(
|
|
11
|
+
return { data: toSkillFrontmatter(parseYamlBlock(raw)), body, raw };
|
|
11
12
|
}
|
|
12
13
|
/** Like parseFrontmatter but returns the raw key/value record instead of
|
|
13
14
|
* coercing to SkillFrontmatter. Used by consumers (e.g. subagents) that read
|
|
@@ -19,138 +20,50 @@ export function parseFrontmatterGeneric(source) {
|
|
|
19
20
|
}
|
|
20
21
|
const raw = match[1];
|
|
21
22
|
const body = source.slice(match[0].length);
|
|
22
|
-
return { data:
|
|
23
|
+
return { data: parseYamlBlock(raw), body, raw };
|
|
23
24
|
}
|
|
24
25
|
function toSkillFrontmatter(out) {
|
|
25
26
|
const fm = {
|
|
26
|
-
name:
|
|
27
|
-
description:
|
|
28
|
-
keywords: Array.isArray(out.keywords) ? out.keywords : undefined,
|
|
27
|
+
name: scalarToString(out.name) ?? '',
|
|
28
|
+
description: scalarToString(out.description),
|
|
29
|
+
keywords: Array.isArray(out.keywords) ? out.keywords.map((k) => String(k)) : undefined,
|
|
29
30
|
type: isSkillType(out.type) ? out.type : undefined,
|
|
30
31
|
};
|
|
31
32
|
return fm;
|
|
32
33
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (r.trim() === '') {
|
|
61
|
-
collected.push('');
|
|
62
|
-
j++;
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
const ind = r.match(/^(\s*)/)?.[1].length ?? 0;
|
|
66
|
-
if (blockIndent === null) {
|
|
67
|
-
if (ind === 0)
|
|
68
|
-
break;
|
|
69
|
-
blockIndent = ind;
|
|
70
|
-
}
|
|
71
|
-
if (ind < blockIndent)
|
|
72
|
-
break;
|
|
73
|
-
collected.push(r.slice(blockIndent));
|
|
74
|
-
j++;
|
|
75
|
-
}
|
|
76
|
-
while (collected.length > 0 && collected[collected.length - 1] === '')
|
|
77
|
-
collected.pop();
|
|
78
|
-
let value;
|
|
79
|
-
if (style === '|') {
|
|
80
|
-
value = collected.join('\n');
|
|
81
|
-
}
|
|
82
|
-
else {
|
|
83
|
-
const parts = [];
|
|
84
|
-
let para = [];
|
|
85
|
-
for (const ln of collected) {
|
|
86
|
-
if (ln === '') {
|
|
87
|
-
if (para.length > 0) {
|
|
88
|
-
parts.push(para.join(' '));
|
|
89
|
-
para = [];
|
|
90
|
-
}
|
|
91
|
-
parts.push('');
|
|
92
|
-
}
|
|
93
|
-
else {
|
|
94
|
-
para.push(ln);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
if (para.length > 0)
|
|
98
|
-
parts.push(para.join(' '));
|
|
99
|
-
const folded = [];
|
|
100
|
-
for (let k = 0; k < parts.length; k++) {
|
|
101
|
-
if (parts[k] === '' && (k === 0 || parts[k - 1] === ''))
|
|
102
|
-
continue;
|
|
103
|
-
folded.push(parts[k]);
|
|
104
|
-
}
|
|
105
|
-
value = folded.join('\n').replace(/\n+$/, '');
|
|
106
|
-
}
|
|
107
|
-
if (chomp !== '+')
|
|
108
|
-
value = value.replace(/\n+$/, '');
|
|
109
|
-
out[key] = value;
|
|
110
|
-
i = j;
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
|
-
// Empty value: could be a list on subsequent lines
|
|
114
|
-
if (rest === '') {
|
|
115
|
-
const buf = [];
|
|
116
|
-
let j = i + 1;
|
|
117
|
-
while (j < lines.length) {
|
|
118
|
-
const r = lines[j];
|
|
119
|
-
if (r.trim() === '') {
|
|
120
|
-
j++;
|
|
121
|
-
continue;
|
|
122
|
-
}
|
|
123
|
-
if (/^\s*-\s+/.test(r)) {
|
|
124
|
-
buf.push(stripQuotes(r.replace(/^\s*-\s+/, '').trim()));
|
|
125
|
-
j++;
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
128
|
-
break;
|
|
129
|
-
}
|
|
130
|
-
if (buf.length > 0)
|
|
131
|
-
out[key] = buf;
|
|
132
|
-
i = j;
|
|
133
|
-
continue;
|
|
134
|
-
}
|
|
135
|
-
if (rest.startsWith('[') && rest.endsWith(']')) {
|
|
136
|
-
out[key] = rest
|
|
137
|
-
.slice(1, -1)
|
|
138
|
-
.split(',')
|
|
139
|
-
.map((s) => stripQuotes(s.trim()))
|
|
140
|
-
.filter(Boolean);
|
|
141
|
-
i++;
|
|
142
|
-
continue;
|
|
143
|
-
}
|
|
144
|
-
out[key] = stripQuotes(rest);
|
|
145
|
-
i++;
|
|
146
|
-
}
|
|
147
|
-
return out;
|
|
34
|
+
/** Coerce a YAML scalar (string/number/boolean) to its string form. A skill's
|
|
35
|
+
* `name`/`description` are always strings to consumers; native non-string
|
|
36
|
+
* scalars (a numeric/boolean value) render as their string form, and
|
|
37
|
+
* non-scalars (null/undefined/array/object) yield undefined. */
|
|
38
|
+
function scalarToString(v) {
|
|
39
|
+
if (typeof v === 'string')
|
|
40
|
+
return v;
|
|
41
|
+
if (typeof v === 'number' || typeof v === 'boolean')
|
|
42
|
+
return String(v);
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
/** Parse the inner YAML of a frontmatter block into a key/value record.
|
|
46
|
+
*
|
|
47
|
+
* ONE strict parser, no fallback. The frontmatter contract IS "valid YAML":
|
|
48
|
+
* the single `yaml`-package path handles the substrate's nested `gate:` maps,
|
|
49
|
+
* inline `{…}` objects, and native scalar typing — and a doc whose frontmatter
|
|
50
|
+
* is NOT valid YAML THROWS, by design. There is deliberately no second lenient
|
|
51
|
+
* parser; a dual parsing strategy is the hedge we reject. Callers that iterate
|
|
52
|
+
* MANY docs isolate a per-doc throw at the COLLECTION layer (a clear scoped
|
|
53
|
+
* notice naming the bad file, then continue); single-doc callers let the error
|
|
54
|
+
* surface so the user learns their one requested doc is malformed.
|
|
55
|
+
*
|
|
56
|
+
* A VALID non-object document (a bare scalar, a top-level list, or an empty
|
|
57
|
+
* block) normalizes to an empty record, preserving the contract that callers
|
|
58
|
+
* always receive a Record for well-formed-but-non-mapping frontmatter. */
|
|
59
|
+
function parseYamlBlock(raw) {
|
|
60
|
+
return normalizeToRecord(parseYaml(raw));
|
|
148
61
|
}
|
|
149
|
-
function
|
|
150
|
-
if (
|
|
151
|
-
return
|
|
62
|
+
function normalizeToRecord(parsed) {
|
|
63
|
+
if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
64
|
+
return parsed;
|
|
152
65
|
}
|
|
153
|
-
return
|
|
66
|
+
return {};
|
|
154
67
|
}
|
|
155
68
|
export function serializeFrontmatter(data) {
|
|
156
69
|
const lines = ['---'];
|
package/dist/core/help.d.ts
CHANGED
|
@@ -162,6 +162,12 @@ export interface LeafHelp {
|
|
|
162
162
|
/** Every persistent change the command makes to the world. For read-only
|
|
163
163
|
* leaves use exactly: ["None. Read-only."] */
|
|
164
164
|
effects: string[];
|
|
165
|
+
/** Bounded runtime aggregate as a complete self-named state element (build it
|
|
166
|
+
* with stateBlock), e.g. `<kinds count="7">…</kinds>`. Lazily evaluated at
|
|
167
|
+
* render time so it reflects the caller's cwd/project scope; appended after
|
|
168
|
+
* the schema. Renderer soft-fails to omission if it returns null or throws.
|
|
169
|
+
* Mirrors BranchHelp.dynamicState. */
|
|
170
|
+
dynamicState?: () => string | null;
|
|
165
171
|
}
|
|
166
172
|
/** Build a self-named runtime-state element: `<tag attr="v">body</tag>`. The
|
|
167
173
|
* subtree that owns the state authors it through this, so the tag name and any
|
package/dist/core/help.js
CHANGED
|
@@ -238,5 +238,12 @@ export function renderLeafArgv(h) {
|
|
|
238
238
|
for (const e of h.effects) {
|
|
239
239
|
lines.push(` ${e}`);
|
|
240
240
|
}
|
|
241
|
+
// Optional bounded runtime-state block (e.g. the live <kinds> list), appended
|
|
242
|
+
// after the schema. Soft-fails to omission on null/throw, mirroring renderBranch.
|
|
243
|
+
const state = evalDynamic(h.dynamicState);
|
|
244
|
+
if (state !== null) {
|
|
245
|
+
lines.push('');
|
|
246
|
+
lines.push(state);
|
|
247
|
+
}
|
|
241
248
|
return lines.join('\n');
|
|
242
249
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Scope } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Thin memory-document resolver for the document substrate. Mirrors the SHAPE
|
|
4
|
+
* of the skill resolver (qualifier parse → scope precedence → direct path
|
|
5
|
+
* lookup → leaf-name fallback) but drops ALL plugin machinery: memory
|
|
6
|
+
* resolution is scope + leaf/path ONLY. The three memory scopes resolve in
|
|
7
|
+
* precedence order project > user > builtin (the same precedence concept as
|
|
8
|
+
* orderPluginsByResolution, minus plugins). It returns the raw parsed
|
|
9
|
+
* frontmatter + body; it does NOT interpret the schema, kind, gate, or rungs —
|
|
10
|
+
* that is the schema/gate layer's job (callers filter by `frontmatter.kind`).
|
|
11
|
+
*/
|
|
12
|
+
export interface MemoryDoc {
|
|
13
|
+
/** Path-derived identity: the doc's path under the scope's memory/ root, no
|
|
14
|
+
* extension, slash-separated — e.g. memory/taste/foo.md → "taste/foo". */
|
|
15
|
+
name: string;
|
|
16
|
+
scope: Scope;
|
|
17
|
+
/** Absolute path to the resolved .md file. */
|
|
18
|
+
path: string;
|
|
19
|
+
/** Raw, uncoerced frontmatter record (null when the doc has no frontmatter). */
|
|
20
|
+
frontmatter: Record<string, unknown> | null;
|
|
21
|
+
/** Document body, with the frontmatter block stripped. */
|
|
22
|
+
body: string;
|
|
23
|
+
}
|
|
24
|
+
export interface MemoryResolutionOpts {
|
|
25
|
+
/** Restrict resolution to a single scope. Conflicts with a scope prefix on
|
|
26
|
+
* the identifier (e.g. `user/foo` with scope=project) throw. */
|
|
27
|
+
scope?: Scope;
|
|
28
|
+
}
|
|
29
|
+
/** Canonical, unambiguous identifier for a memory document: `<scope>/<name>`. */
|
|
30
|
+
export declare function memoryDocId(doc: MemoryDoc): string;
|
|
31
|
+
/** All memory docs in one scope's memory/ dir, scanned recursively for *.md
|
|
32
|
+
* (topical subdirs supported), sorted by path-derived name. */
|
|
33
|
+
export declare function listMemoryDocs(scope: Scope): MemoryDoc[];
|
|
34
|
+
/** All memory docs across the resolved scopes, in precedence order
|
|
35
|
+
* (project, then user, then builtin), each scope's docs name-sorted within. */
|
|
36
|
+
export declare function listAllMemoryDocs(scope?: Scope): MemoryDoc[];
|
|
37
|
+
/**
|
|
38
|
+
* Resolve a path-derived name to a single memory document.
|
|
39
|
+
*
|
|
40
|
+
* Accepted identifier forms (mirroring parseSkillQualifier, no plugins):
|
|
41
|
+
* <name> — bare name; resolved by scope precedence project>user>builtin
|
|
42
|
+
* <scope>/<name> — pinned to one scope (user|project)
|
|
43
|
+
* `<name>` may carry topical subdirs (`taste/foo`); a bare leaf (`foo`) falls
|
|
44
|
+
* back to a last-segment match across the resolved scopes.
|
|
45
|
+
*
|
|
46
|
+
* Resolution order: parse qualifier → direct memory/<name>.md lookup
|
|
47
|
+
* (precedence-first) → leaf-name fallback (ambiguity error listing candidates).
|
|
48
|
+
*/
|
|
49
|
+
export declare function resolveMemoryDoc(rawName: string, opts?: MemoryResolutionOpts): MemoryDoc;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { join, relative, sep } from 'node:path';
|
|
2
|
+
import { pathExists, readText, walkFiles } from './fs-utils.js';
|
|
3
|
+
import { parseFrontmatterGeneric } from './frontmatter.js';
|
|
4
|
+
import { parseSkillQualifier } from './resolver.js';
|
|
5
|
+
import { ambiguous, notFound, usage } from './errors.js';
|
|
6
|
+
import { warn } from './output.js';
|
|
7
|
+
import { projectScopeRoot, scopeMemoryDir } from './scope.js';
|
|
8
|
+
/** Canonical, unambiguous identifier for a memory document: `<scope>/<name>`. */
|
|
9
|
+
export function memoryDocId(doc) {
|
|
10
|
+
return `${doc.scope}/${doc.name}`;
|
|
11
|
+
}
|
|
12
|
+
/** The memory scopes in resolution precedence: project > user > builtin.
|
|
13
|
+
* Project is included only when a project scope exists for the cwd. A single
|
|
14
|
+
* `scope` narrows to just that scope. */
|
|
15
|
+
function scopesInPrecedence(scope) {
|
|
16
|
+
if (scope)
|
|
17
|
+
return [scope];
|
|
18
|
+
const out = [];
|
|
19
|
+
if (projectScopeRoot())
|
|
20
|
+
out.push('project');
|
|
21
|
+
out.push('user');
|
|
22
|
+
out.push('builtin');
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
function loadMemoryDoc(name, scope, path) {
|
|
26
|
+
const { data, body } = parseFrontmatterGeneric(readText(path));
|
|
27
|
+
return { name, scope, path, frontmatter: data, body };
|
|
28
|
+
}
|
|
29
|
+
/** All memory docs in one scope's memory/ dir, scanned recursively for *.md
|
|
30
|
+
* (topical subdirs supported), sorted by path-derived name. */
|
|
31
|
+
export function listMemoryDocs(scope) {
|
|
32
|
+
const dir = scopeMemoryDir(scope);
|
|
33
|
+
if (!dir || !pathExists(dir))
|
|
34
|
+
return [];
|
|
35
|
+
const docs = [];
|
|
36
|
+
for (const file of walkFiles(dir, (n) => n.endsWith('.md'))) {
|
|
37
|
+
const name = relative(dir, file).replace(/\.md$/i, '').split(sep).join('/');
|
|
38
|
+
if (!name)
|
|
39
|
+
continue;
|
|
40
|
+
// COLLECTION layer: the strict frontmatter parser throws on invalid YAML.
|
|
41
|
+
// Isolate one malformed doc with a clear scoped notice + skip, so a single
|
|
42
|
+
// bad file can't brick `memory list`/`find` or the substrate boot render.
|
|
43
|
+
try {
|
|
44
|
+
docs.push(loadMemoryDoc(name, scope, file));
|
|
45
|
+
}
|
|
46
|
+
catch (e) {
|
|
47
|
+
const msg = (e instanceof Error ? e.message : String(e)).split('\n')[0];
|
|
48
|
+
warn(`invalid frontmatter in ${file}: ${msg}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return docs.sort((a, b) => a.name.localeCompare(b.name));
|
|
52
|
+
}
|
|
53
|
+
/** All memory docs across the resolved scopes, in precedence order
|
|
54
|
+
* (project, then user, then builtin), each scope's docs name-sorted within. */
|
|
55
|
+
export function listAllMemoryDocs(scope) {
|
|
56
|
+
return scopesInPrecedence(scope).flatMap(listMemoryDocs);
|
|
57
|
+
}
|
|
58
|
+
/** Direct full-path lookup of memory/<name>.md across scopes, precedence-first.
|
|
59
|
+
* Returns every scope's hit in precedence order; the resolver takes the first
|
|
60
|
+
* (highest-precedence) one — a fully-specified name is never ambiguous. */
|
|
61
|
+
function findMemoryMatches(name, scope) {
|
|
62
|
+
const matches = [];
|
|
63
|
+
for (const s of scopesInPrecedence(scope)) {
|
|
64
|
+
const dir = scopeMemoryDir(s);
|
|
65
|
+
if (!dir)
|
|
66
|
+
continue;
|
|
67
|
+
const path = join(dir, ...name.split('/')) + '.md';
|
|
68
|
+
if (!pathExists(path))
|
|
69
|
+
continue;
|
|
70
|
+
matches.push(loadMemoryDoc(name, s, path));
|
|
71
|
+
}
|
|
72
|
+
return matches;
|
|
73
|
+
}
|
|
74
|
+
/** Leaf-name fallback: match docs whose final path segment equals `leaf`.
|
|
75
|
+
* Only meaningful for a bare segment (a slashed query can never equal a single
|
|
76
|
+
* segment), mirroring findSkillsByLeaf. Returns matches precedence-ordered. */
|
|
77
|
+
function findMemoryByLeaf(leaf, scope) {
|
|
78
|
+
if (leaf.includes('/'))
|
|
79
|
+
return [];
|
|
80
|
+
let all;
|
|
81
|
+
try {
|
|
82
|
+
all = listAllMemoryDocs(scope);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
return all.filter((d) => (d.name.split('/').pop() ?? d.name) === leaf);
|
|
88
|
+
}
|
|
89
|
+
function formatLeafAmbiguous(leaf, matches) {
|
|
90
|
+
const ids = matches.map(memoryDocId).join(', ');
|
|
91
|
+
return `ambiguous memory document: ${leaf} matches multiple documents: ${ids}`;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Resolve a path-derived name to a single memory document.
|
|
95
|
+
*
|
|
96
|
+
* Accepted identifier forms (mirroring parseSkillQualifier, no plugins):
|
|
97
|
+
* <name> — bare name; resolved by scope precedence project>user>builtin
|
|
98
|
+
* <scope>/<name> — pinned to one scope (user|project)
|
|
99
|
+
* `<name>` may carry topical subdirs (`taste/foo`); a bare leaf (`foo`) falls
|
|
100
|
+
* back to a last-segment match across the resolved scopes.
|
|
101
|
+
*
|
|
102
|
+
* Resolution order: parse qualifier → direct memory/<name>.md lookup
|
|
103
|
+
* (precedence-first) → leaf-name fallback (ambiguity error listing candidates).
|
|
104
|
+
*/
|
|
105
|
+
export function resolveMemoryDoc(rawName, opts = {}) {
|
|
106
|
+
const parsed = parseSkillQualifier(rawName);
|
|
107
|
+
if (parsed.scope && opts.scope && parsed.scope !== opts.scope) {
|
|
108
|
+
throw usage(`scope conflict: identifier "${rawName}" uses scope "${parsed.scope}" but --scope is "${opts.scope}"`);
|
|
109
|
+
}
|
|
110
|
+
const effectiveScope = opts.scope ?? parsed.scope;
|
|
111
|
+
const name = parsed.segments.join('/');
|
|
112
|
+
if (name === '')
|
|
113
|
+
throw usage(`memory document name required`);
|
|
114
|
+
// Direct full-path lookup: a fully-specified name resolves by scope precedence.
|
|
115
|
+
const direct = findMemoryMatches(name, effectiveScope);
|
|
116
|
+
if (direct.length > 0)
|
|
117
|
+
return direct[0];
|
|
118
|
+
// Leaf-name fallback: the caller gave only the final segment (e.g. "foo" for
|
|
119
|
+
// "taste/foo"). Match by last segment across the resolved scope dirs.
|
|
120
|
+
const byLeaf = findMemoryByLeaf(name, effectiveScope);
|
|
121
|
+
if (byLeaf.length > 0) {
|
|
122
|
+
// Same path-derived name across scopes → precedence wins (return first);
|
|
123
|
+
// genuinely different docs sharing a leaf → ambiguous.
|
|
124
|
+
const distinctNames = new Set(byLeaf.map((d) => d.name));
|
|
125
|
+
if (distinctNames.size === 1)
|
|
126
|
+
return byLeaf[0];
|
|
127
|
+
throw ambiguous(formatLeafAmbiguous(name, byLeaf), {
|
|
128
|
+
memory: name,
|
|
129
|
+
candidates: byLeaf.map((d) => ({
|
|
130
|
+
id: memoryDocId(d),
|
|
131
|
+
scope: d.scope,
|
|
132
|
+
path: d.path,
|
|
133
|
+
})),
|
|
134
|
+
next: 'Multiple documents share this leaf name. Re-run with one of the full names in candidates.',
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
throw notFound(`memory document not found: ${rawName}`, {
|
|
138
|
+
memory: name,
|
|
139
|
+
scope: parsed.scope,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
* Persona composer public surface.
|
|
3
3
|
*
|
|
4
4
|
* Re-exports:
|
|
5
|
-
* - loadPersona / loadKernel / availableKinds
|
|
5
|
+
* - loadPersona / loadKernel / availableKinds / kindWhenToUse / subPersonasFor
|
|
6
|
+
* (loader — raw file access)
|
|
6
7
|
* - resolve (high-level composer)
|
|
7
8
|
* - ResolvedPersona (return type of resolve)
|
|
8
9
|
*/
|
|
9
|
-
export { loadPersona, loadKernel, availableKinds, loadLifecycleFragment, loadSpineFragment } from './loader.js';
|
|
10
|
-
export type { LoadedPersona } from './loader.js';
|
|
10
|
+
export { loadPersona, loadKernel, availableKinds, kindWhenToUse, subPersonasFor, loadLifecycleFragment, loadSpineFragment } from './loader.js';
|
|
11
|
+
export type { LoadedPersona, SubPersona } from './loader.js';
|
|
11
12
|
export { resolve } from './resolve.js';
|
|
12
13
|
export type { ResolvedPersona } from './resolve.js';
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
* Persona composer public surface.
|
|
3
3
|
*
|
|
4
4
|
* Re-exports:
|
|
5
|
-
* - loadPersona / loadKernel / availableKinds
|
|
5
|
+
* - loadPersona / loadKernel / availableKinds / kindWhenToUse / subPersonasFor
|
|
6
|
+
* (loader — raw file access)
|
|
6
7
|
* - resolve (high-level composer)
|
|
7
8
|
* - ResolvedPersona (return type of resolve)
|
|
8
9
|
*/
|
|
9
|
-
export { loadPersona, loadKernel, availableKinds, loadLifecycleFragment, loadSpineFragment } from './loader.js';
|
|
10
|
+
export { loadPersona, loadKernel, availableKinds, kindWhenToUse, subPersonasFor, loadLifecycleFragment, loadSpineFragment } from './loader.js';
|
|
10
11
|
export { resolve } from './resolve.js';
|