@crouton-kit/crouter 0.3.8 → 0.3.12
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/bin/crtrd +2 -0
- package/dist/builtin-personas/design/base.md +9 -0
- package/dist/builtin-personas/design/orchestrator.md +10 -0
- package/dist/builtin-personas/developer/base.md +9 -0
- package/dist/builtin-personas/developer/orchestrator.md +12 -0
- package/dist/builtin-personas/explore/base.md +9 -0
- package/dist/builtin-personas/explore/orchestrator.md +9 -0
- package/dist/builtin-personas/general/base.md +5 -0
- package/dist/builtin-personas/general/orchestrator.md +7 -0
- package/dist/builtin-personas/orchestration-kernel.md +71 -0
- package/dist/builtin-personas/plan/base.md +7 -0
- package/dist/builtin-personas/plan/orchestrator.md +12 -0
- package/dist/builtin-personas/review/base.md +7 -0
- package/dist/builtin-personas/review/orchestrator.md +9 -0
- package/dist/builtin-personas/runtime-base.md +39 -0
- package/dist/builtin-personas/spec/base.md +7 -0
- package/dist/builtin-personas/spec/orchestrator.md +10 -0
- package/dist/builtin-skills/skills/design/SKILL.md +51 -0
- package/dist/builtin-skills/skills/development/SKILL.md +109 -0
- package/dist/builtin-skills/skills/planning/SKILL.md +59 -0
- package/dist/builtin-skills/skills/spec/SKILL.md +83 -0
- package/dist/cli.js +25 -27
- package/dist/commands/{job.d.ts → attention.d.ts} +1 -1
- package/dist/commands/attention.js +152 -0
- package/dist/commands/canvas.d.ts +2 -0
- package/dist/commands/canvas.js +35 -0
- package/dist/commands/{agent.d.ts → daemon.d.ts} +1 -1
- package/dist/commands/daemon.js +111 -0
- package/dist/commands/dashboard.d.ts +2 -0
- package/dist/commands/dashboard.js +65 -0
- package/dist/commands/human/prompts.d.ts +5 -0
- package/dist/commands/human/prompts.js +269 -0
- package/dist/commands/human/queue.d.ts +3 -0
- package/dist/commands/human/queue.js +133 -0
- package/dist/commands/human/shared.d.ts +43 -0
- package/dist/commands/human/shared.js +107 -0
- package/dist/commands/human.js +15 -427
- package/dist/commands/node.d.ts +2 -0
- package/dist/commands/node.js +354 -0
- package/dist/commands/pkg/market-inspect.d.ts +1 -0
- package/dist/commands/pkg/market-inspect.js +157 -0
- package/dist/commands/pkg/market-manage.d.ts +1 -0
- package/dist/commands/pkg/market-manage.js +316 -0
- package/dist/commands/pkg/market.d.ts +1 -0
- package/dist/commands/pkg/market.js +16 -0
- package/dist/commands/pkg/plugin-inspect.d.ts +1 -0
- package/dist/commands/pkg/plugin-inspect.js +142 -0
- package/dist/commands/pkg/plugin-manage.d.ts +1 -0
- package/dist/commands/pkg/plugin-manage.js +294 -0
- package/dist/commands/pkg/plugin.d.ts +1 -0
- package/dist/commands/pkg/plugin.js +16 -0
- package/dist/commands/pkg/shared.d.ts +5 -0
- package/dist/commands/pkg/shared.js +61 -0
- package/dist/commands/pkg.js +8 -1004
- package/dist/commands/push.d.ts +3 -0
- package/dist/commands/push.js +159 -0
- package/dist/commands/revive.d.ts +2 -0
- package/dist/commands/revive.js +64 -0
- package/dist/commands/skill/author.d.ts +3 -0
- package/dist/commands/skill/author.js +147 -0
- package/dist/commands/skill/find.d.ts +4 -0
- package/dist/commands/skill/find.js +254 -0
- package/dist/commands/skill/read.d.ts +1 -0
- package/dist/commands/skill/read.js +89 -0
- package/dist/commands/skill/shared.d.ts +19 -0
- package/dist/commands/skill/shared.js +207 -0
- package/dist/commands/skill/state.d.ts +3 -0
- package/dist/commands/skill/state.js +69 -0
- package/dist/commands/skill.js +12 -681
- package/dist/commands/sys/config.d.ts +1 -0
- package/dist/commands/sys/config.js +186 -0
- package/dist/commands/sys/doctor.d.ts +1 -0
- package/dist/commands/sys/doctor.js +369 -0
- package/dist/commands/sys/shared.d.ts +3 -0
- package/dist/commands/sys/shared.js +24 -0
- package/dist/commands/sys/update.d.ts +2 -0
- package/dist/commands/sys/update.js +114 -0
- package/dist/commands/sys.js +9 -694
- package/dist/core/__tests__/argv-parser.test.js +19 -1
- package/dist/core/__tests__/canvas-inbox-watcher.test.js +100 -0
- package/dist/core/__tests__/canvas.test.js +154 -0
- package/dist/core/__tests__/reset.test.js +105 -0
- package/dist/core/__tests__/resolver.test.js +69 -1
- package/dist/core/__tests__/unknown-path.test.d.ts +1 -0
- package/dist/core/__tests__/unknown-path.test.js +52 -0
- package/dist/core/bootstrap.d.ts +2 -0
- package/dist/core/bootstrap.js +66 -0
- package/dist/core/canvas/attention.d.ts +24 -0
- package/dist/core/canvas/attention.js +94 -0
- package/dist/core/canvas/canvas.d.ts +40 -0
- package/dist/core/canvas/canvas.js +210 -0
- package/dist/core/canvas/db.d.ts +7 -0
- package/dist/core/canvas/db.js +61 -0
- package/dist/core/canvas/index.d.ts +4 -0
- package/dist/core/canvas/index.js +6 -0
- package/dist/core/canvas/paths.d.ts +16 -0
- package/dist/core/canvas/paths.js +62 -0
- package/dist/core/canvas/render.d.ts +30 -0
- package/dist/core/canvas/render.js +186 -0
- package/dist/core/canvas/types.d.ts +87 -0
- package/dist/core/canvas/types.js +8 -0
- package/dist/core/command.d.ts +63 -2
- package/dist/core/command.js +97 -24
- package/dist/core/feed/feed.d.ts +43 -0
- package/dist/core/feed/feed.js +116 -0
- package/dist/core/feed/inbox.d.ts +50 -0
- package/dist/core/feed/inbox.js +124 -0
- package/dist/core/frontmatter.d.ts +10 -0
- package/dist/core/frontmatter.js +24 -9
- package/dist/core/help.d.ts +39 -8
- package/dist/core/help.js +69 -35
- package/dist/core/io.d.ts +15 -1
- package/dist/core/io.js +56 -6
- package/dist/core/personas/index.d.ts +12 -0
- package/dist/core/personas/index.js +10 -0
- package/dist/core/personas/loader.d.ts +44 -0
- package/dist/core/personas/loader.js +157 -0
- package/dist/core/personas/resolve.d.ts +36 -0
- package/dist/core/personas/resolve.js +110 -0
- package/dist/core/render.d.ts +11 -0
- package/dist/core/render.js +126 -0
- package/dist/core/resolver.d.ts +10 -0
- package/dist/core/resolver.js +160 -2
- package/dist/core/runtime/front-door.d.ts +10 -0
- package/dist/core/runtime/front-door.js +97 -0
- package/dist/core/runtime/kickoff.d.ts +23 -0
- package/dist/core/runtime/kickoff.js +134 -0
- package/dist/core/runtime/launch.d.ts +34 -0
- package/dist/core/runtime/launch.js +85 -0
- package/dist/core/runtime/nodes.d.ts +38 -0
- package/dist/core/runtime/nodes.js +95 -0
- package/dist/core/runtime/presence.d.ts +38 -0
- package/dist/core/runtime/presence.js +152 -0
- package/dist/core/runtime/promote.d.ts +30 -0
- package/dist/core/runtime/promote.js +105 -0
- package/dist/core/runtime/reset.d.ts +13 -0
- package/dist/core/runtime/reset.js +97 -0
- package/dist/core/runtime/revive.d.ts +26 -0
- package/dist/core/runtime/revive.js +89 -0
- package/dist/core/runtime/roadmap.d.ts +12 -0
- package/dist/core/runtime/roadmap.js +52 -0
- package/dist/core/runtime/spawn.d.ts +33 -0
- package/dist/core/runtime/spawn.js +118 -0
- package/dist/core/runtime/stop-guard.d.ts +18 -0
- package/dist/core/runtime/stop-guard.js +33 -0
- package/dist/core/runtime/tmux.d.ts +88 -0
- package/dist/core/runtime/tmux.js +198 -0
- package/dist/core/spawn.d.ts +17 -80
- package/dist/core/spawn.js +15 -219
- package/dist/daemon/crtrd-cli.d.ts +1 -0
- package/dist/daemon/crtrd-cli.js +4 -0
- package/dist/daemon/crtrd.d.ts +20 -0
- package/dist/daemon/crtrd.js +200 -0
- package/dist/daemon/manage.d.ts +17 -0
- package/dist/daemon/manage.js +57 -0
- package/dist/pi-extensions/canvas-inbox-watcher.d.ts +16 -0
- package/dist/pi-extensions/canvas-inbox-watcher.js +229 -0
- package/dist/pi-extensions/canvas-nav.d.ts +32 -0
- package/dist/pi-extensions/canvas-nav.js +536 -0
- package/dist/pi-extensions/canvas-stophook.d.ts +17 -0
- package/dist/pi-extensions/canvas-stophook.js +373 -0
- package/dist/types.d.ts +21 -0
- package/dist/types.js +3 -0
- package/package.json +6 -5
- package/dist/commands/agent.js +0 -384
- package/dist/commands/debug.d.ts +0 -3
- package/dist/commands/debug.js +0 -179
- package/dist/commands/job.js +0 -344
- package/dist/commands/plan.d.ts +0 -4
- package/dist/commands/plan.js +0 -309
- package/dist/commands/spec.d.ts +0 -3
- package/dist/commands/spec.js +0 -286
- package/dist/core/__tests__/flow-leaves.test.js +0 -248
- package/dist/core/__tests__/job.test.js +0 -310
- package/dist/core/__tests__/jobs.test.js +0 -66
- package/dist/core/jobs.d.ts +0 -101
- package/dist/core/jobs.js +0 -462
- package/dist/prompts/agent.d.ts +0 -18
- package/dist/prompts/agent.js +0 -153
- package/dist/prompts/debug.d.ts +0 -8
- package/dist/prompts/debug.js +0 -44
- /package/dist/core/__tests__/{flow-leaves.test.d.ts → canvas-inbox-watcher.test.d.ts} +0 -0
- /package/dist/core/__tests__/{job.test.d.ts → canvas.test.d.ts} +0 -0
- /package/dist/core/__tests__/{jobs.test.d.ts → reset.test.d.ts} +0 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// Push engine for the pi-native canvas runtime.
|
|
2
|
+
//
|
|
3
|
+
// `push(nodeId, opts)` writes a report to the node's reports/ directory, then
|
|
4
|
+
// fans out a lightweight inbox pointer to every subscriber. The inbox entry
|
|
5
|
+
// carries the report path (ref), not the body — subscribers dereference on
|
|
6
|
+
// demand.
|
|
7
|
+
//
|
|
8
|
+
// Compact timestamp format: 20260602T184512 (UTC, no separators) chosen for
|
|
9
|
+
// file-system friendliness and lexicographic sort alignment.
|
|
10
|
+
import { writeFileSync, renameSync, mkdirSync, existsSync } from 'node:fs';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { reportsDir, subscribersOf, setStatus, updateNode, } from '../canvas/index.js';
|
|
13
|
+
import { appendInbox } from './inbox.js';
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Internal helpers
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
/** Format a Date as `YYYYMMDDTHHmmss` (UTC, no separators). */
|
|
18
|
+
function compactTs(d = new Date()) {
|
|
19
|
+
const pad = (n, w = 2) => String(n).padStart(w, '0');
|
|
20
|
+
return (String(d.getUTCFullYear()) +
|
|
21
|
+
pad(d.getUTCMonth() + 1) +
|
|
22
|
+
pad(d.getUTCDate()) +
|
|
23
|
+
'T' +
|
|
24
|
+
pad(d.getUTCHours()) +
|
|
25
|
+
pad(d.getUTCMinutes()) +
|
|
26
|
+
pad(d.getUTCSeconds()));
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Write a report file atomically (tmp + rename).
|
|
30
|
+
* Returns the final absolute path.
|
|
31
|
+
*/
|
|
32
|
+
function writeReport(nodeId, kind, ts, body) {
|
|
33
|
+
const dir = reportsDir(nodeId);
|
|
34
|
+
if (!existsSync(dir))
|
|
35
|
+
mkdirSync(dir, { recursive: true });
|
|
36
|
+
const fileName = `${ts}-${kind}.md`;
|
|
37
|
+
const finalPath = join(dir, fileName);
|
|
38
|
+
const tmpPath = `${finalPath}.tmp`;
|
|
39
|
+
const isoTs = new Date().toISOString();
|
|
40
|
+
// YAML frontmatter: minimal, machine-readable, no freeform content in it.
|
|
41
|
+
const frontmatter = `---\nnode: ${nodeId}\nkind: ${kind}\nts: ${isoTs}\n---\n`;
|
|
42
|
+
writeFileSync(tmpPath, frontmatter + body, 'utf8');
|
|
43
|
+
renameSync(tmpPath, finalPath);
|
|
44
|
+
return finalPath;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Extract the first line of a string and truncate to `maxLen` chars.
|
|
48
|
+
* Used to populate the inbox entry's `label` field (~80 chars).
|
|
49
|
+
*/
|
|
50
|
+
function firstLine(text, maxLen = 80) {
|
|
51
|
+
const line = text.split('\n')[0] ?? '';
|
|
52
|
+
return line.length > maxLen ? line.slice(0, maxLen - 1) + '…' : line;
|
|
53
|
+
}
|
|
54
|
+
/** Map a PushKind to the appropriate inbox delivery tier. */
|
|
55
|
+
function tierFor(kind) {
|
|
56
|
+
return kind === 'urgent' ? 'urgent' : 'normal';
|
|
57
|
+
}
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// Core push
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
/**
|
|
62
|
+
* Push a report from `nodeId` and fan it out as inbox pointers to all
|
|
63
|
+
* current subscribers.
|
|
64
|
+
*
|
|
65
|
+
* Steps:
|
|
66
|
+
* (a) Write nodes/<nodeId>/reports/<ts>-<kind>.md (YAML front + body).
|
|
67
|
+
* (b) For each active/passive subscriber, append a pointer to their inbox.
|
|
68
|
+
* (c) If kind === 'final', mark the node done.
|
|
69
|
+
*/
|
|
70
|
+
export async function push(nodeId, opts) {
|
|
71
|
+
const { kind, body } = opts;
|
|
72
|
+
const from = opts.from ?? nodeId;
|
|
73
|
+
const now = new Date();
|
|
74
|
+
const ts = compactTs(now);
|
|
75
|
+
// (a) Write the report.
|
|
76
|
+
const reportPath = writeReport(nodeId, kind, ts, body);
|
|
77
|
+
// (b) Fan out inbox pointers to every subscriber (active and passive both
|
|
78
|
+
// receive the pointer; the daemon decides whether to wake active ones).
|
|
79
|
+
const subscribers = subscribersOf(nodeId);
|
|
80
|
+
const deliveredTo = [];
|
|
81
|
+
const label = firstLine(body);
|
|
82
|
+
for (const sub of subscribers) {
|
|
83
|
+
appendInbox(sub.node_id, {
|
|
84
|
+
from,
|
|
85
|
+
tier: tierFor(kind),
|
|
86
|
+
kind,
|
|
87
|
+
ref: reportPath,
|
|
88
|
+
label,
|
|
89
|
+
});
|
|
90
|
+
deliveredTo.push(sub.node_id);
|
|
91
|
+
}
|
|
92
|
+
// (c) Finalise node when kind === 'final'.
|
|
93
|
+
if (kind === 'final') {
|
|
94
|
+
setStatus(nodeId, 'done');
|
|
95
|
+
updateNode(nodeId, { intent: 'done' });
|
|
96
|
+
}
|
|
97
|
+
return { reportPath, deliveredTo };
|
|
98
|
+
}
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// Convenience wrappers
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
/** Emit a routine progress update from `nodeId`. */
|
|
103
|
+
export async function pushUpdate(nodeId, body, opts) {
|
|
104
|
+
return push(nodeId, { kind: 'update', body, ...opts });
|
|
105
|
+
}
|
|
106
|
+
/** Emit an urgent alert from `nodeId` (inbox tier: urgent). */
|
|
107
|
+
export async function pushUrgent(nodeId, body, opts) {
|
|
108
|
+
return push(nodeId, { kind: 'urgent', body, ...opts });
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Emit the final report from `nodeId` (inbox tier: normal, kind: final).
|
|
112
|
+
* Also transitions the node to status=done / intent=done.
|
|
113
|
+
*/
|
|
114
|
+
export async function pushFinal(nodeId, body, opts) {
|
|
115
|
+
return push(nodeId, { kind: 'final', body, ...opts });
|
|
116
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export type InboxTier = 'critical' | 'urgent' | 'normal' | 'deferred';
|
|
2
|
+
export type InboxKind = 'update' | 'urgent' | 'final' | 'message' | 'completed';
|
|
3
|
+
/** A single inbox entry — a pointer, not a copy of the content. */
|
|
4
|
+
export interface InboxEntry {
|
|
5
|
+
/** ISO 8601 timestamp of delivery. */
|
|
6
|
+
ts: string;
|
|
7
|
+
/** Node id of the sender, or null for system-generated entries. */
|
|
8
|
+
from: string | null;
|
|
9
|
+
/** Priority band for the receiver's attention. */
|
|
10
|
+
tier: InboxTier;
|
|
11
|
+
/** Semantic kind of the push event. */
|
|
12
|
+
kind: InboxKind;
|
|
13
|
+
/** Absolute path to the report file, when this entry is a push pointer. */
|
|
14
|
+
ref?: string;
|
|
15
|
+
/** First ~80 chars of the body's first line — enough to decide if it matters. */
|
|
16
|
+
label: string;
|
|
17
|
+
/** Arbitrary structured payload for non-push message entries. */
|
|
18
|
+
data?: Record<string, unknown>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Atomically append one inbox entry to `nodes/<nodeId>/inbox.jsonl`.
|
|
22
|
+
* Fills `ts` (current ISO time). Returns the completed entry.
|
|
23
|
+
*/
|
|
24
|
+
export declare function appendInbox(nodeId: string, entry: Omit<InboxEntry, 'ts'>): InboxEntry;
|
|
25
|
+
/**
|
|
26
|
+
* Return all inbox entries strictly after `cursorIso`.
|
|
27
|
+
* When `cursorIso` is undefined, returns every entry in the file.
|
|
28
|
+
*/
|
|
29
|
+
export declare function readInboxSince(nodeId: string, cursorIso?: string): InboxEntry[];
|
|
30
|
+
/**
|
|
31
|
+
* Read the persisted cursor ISO for a node's inbox.
|
|
32
|
+
* Returns undefined if no cursor file exists yet.
|
|
33
|
+
*/
|
|
34
|
+
export declare function readCursor(nodeId: string): string | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* Persist a new cursor ISO for a node's inbox (atomic tmp+rename).
|
|
37
|
+
*/
|
|
38
|
+
export declare function writeCursor(nodeId: string, iso: string): void;
|
|
39
|
+
/**
|
|
40
|
+
* Render many unread inbox pointers into one compact digest string.
|
|
41
|
+
*
|
|
42
|
+
* Format (per sender group):
|
|
43
|
+
* From <sender> — <N> update(s):
|
|
44
|
+
* [<kind>] <label> (ref: <path>)
|
|
45
|
+
* …
|
|
46
|
+
*
|
|
47
|
+
* A header line announces the total count and instructs the receiver to
|
|
48
|
+
* dereference only what matters.
|
|
49
|
+
*/
|
|
50
|
+
export declare function coalesce(entries: InboxEntry[]): string;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// Per-node inbox.jsonl primitive for the pi-native canvas runtime.
|
|
2
|
+
//
|
|
3
|
+
// An inbox entry is a lightweight POINTER (~30 tokens), never content.
|
|
4
|
+
// The report body lives in nodes/<id>/reports/; the inbox line carries only
|
|
5
|
+
// enough to find it and decide whether to dereference.
|
|
6
|
+
//
|
|
7
|
+
// Layout:
|
|
8
|
+
// nodes/<id>/inbox.jsonl — one JSON line per entry, append-only
|
|
9
|
+
// nodes/<id>/inbox.jsonl.cursor — ISO 8601 of last-read entry (sidecar)
|
|
10
|
+
import { appendFileSync, existsSync, readFileSync, writeFileSync, renameSync, mkdirSync, } from 'node:fs';
|
|
11
|
+
import { dirname } from 'node:path';
|
|
12
|
+
import { inboxPath } from '../canvas/index.js';
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Cursor sidecar path
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
function cursorPath(nodeId) {
|
|
17
|
+
return `${inboxPath(nodeId)}.cursor`;
|
|
18
|
+
}
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Append
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
/**
|
|
23
|
+
* Atomically append one inbox entry to `nodes/<nodeId>/inbox.jsonl`.
|
|
24
|
+
* Fills `ts` (current ISO time). Returns the completed entry.
|
|
25
|
+
*/
|
|
26
|
+
export function appendInbox(nodeId, entry) {
|
|
27
|
+
const full = { ts: new Date().toISOString(), ...entry };
|
|
28
|
+
const line = JSON.stringify(full) + '\n';
|
|
29
|
+
// Ensure the parent directory exists (inbox.jsonl lives directly under the
|
|
30
|
+
// node dir, which ensureNodeDirs creates — but guard anyway for callers that
|
|
31
|
+
// haven't yet scaffolded the node).
|
|
32
|
+
const dir = dirname(inboxPath(nodeId));
|
|
33
|
+
if (!existsSync(dir))
|
|
34
|
+
mkdirSync(dir, { recursive: true });
|
|
35
|
+
// appendFileSync is atomic within a single process (a single write(2) call for
|
|
36
|
+
// a short line is atomic on POSIX). For multi-process safety we rely on the
|
|
37
|
+
// OS-level append guarantee (O_APPEND) which Node honours via 'a' flag.
|
|
38
|
+
appendFileSync(inboxPath(nodeId), line, { encoding: 'utf8', flag: 'a' });
|
|
39
|
+
return full;
|
|
40
|
+
}
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Read
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
/**
|
|
45
|
+
* Return all inbox entries strictly after `cursorIso`.
|
|
46
|
+
* When `cursorIso` is undefined, returns every entry in the file.
|
|
47
|
+
*/
|
|
48
|
+
export function readInboxSince(nodeId, cursorIso) {
|
|
49
|
+
const p = inboxPath(nodeId);
|
|
50
|
+
if (!existsSync(p))
|
|
51
|
+
return [];
|
|
52
|
+
const raw = readFileSync(p, 'utf8');
|
|
53
|
+
const entries = raw
|
|
54
|
+
.split('\n')
|
|
55
|
+
.filter((l) => l.trim() !== '')
|
|
56
|
+
.map((l) => JSON.parse(l));
|
|
57
|
+
if (cursorIso === undefined)
|
|
58
|
+
return entries;
|
|
59
|
+
// Entries are appended in chronological order; filter to those strictly
|
|
60
|
+
// after the cursor. We compare ISO strings lexicographically (valid for UTC).
|
|
61
|
+
return entries.filter((e) => e.ts > cursorIso);
|
|
62
|
+
}
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// Cursor persistence
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
/**
|
|
67
|
+
* Read the persisted cursor ISO for a node's inbox.
|
|
68
|
+
* Returns undefined if no cursor file exists yet.
|
|
69
|
+
*/
|
|
70
|
+
export function readCursor(nodeId) {
|
|
71
|
+
const p = cursorPath(nodeId);
|
|
72
|
+
if (!existsSync(p))
|
|
73
|
+
return undefined;
|
|
74
|
+
const val = readFileSync(p, 'utf8').trim();
|
|
75
|
+
return val !== '' ? val : undefined;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Persist a new cursor ISO for a node's inbox (atomic tmp+rename).
|
|
79
|
+
*/
|
|
80
|
+
export function writeCursor(nodeId, iso) {
|
|
81
|
+
const p = cursorPath(nodeId);
|
|
82
|
+
const tmp = `${p}.tmp`;
|
|
83
|
+
const dir = dirname(p);
|
|
84
|
+
if (!existsSync(dir))
|
|
85
|
+
mkdirSync(dir, { recursive: true });
|
|
86
|
+
writeFileSync(tmp, iso, 'utf8');
|
|
87
|
+
renameSync(tmp, p);
|
|
88
|
+
}
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// Coalesce
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
/**
|
|
93
|
+
* Render many unread inbox pointers into one compact digest string.
|
|
94
|
+
*
|
|
95
|
+
* Format (per sender group):
|
|
96
|
+
* From <sender> — <N> update(s):
|
|
97
|
+
* [<kind>] <label> (ref: <path>)
|
|
98
|
+
* …
|
|
99
|
+
*
|
|
100
|
+
* A header line announces the total count and instructs the receiver to
|
|
101
|
+
* dereference only what matters.
|
|
102
|
+
*/
|
|
103
|
+
export function coalesce(entries) {
|
|
104
|
+
if (entries.length === 0)
|
|
105
|
+
return '(inbox empty)';
|
|
106
|
+
const header = `${entries.length} update${entries.length === 1 ? '' : 's'} since last read — dereference what matters.\n`;
|
|
107
|
+
// Group by `from` (null → 'system').
|
|
108
|
+
const groups = new Map();
|
|
109
|
+
for (const e of entries) {
|
|
110
|
+
const key = e.from ?? 'system';
|
|
111
|
+
if (!groups.has(key))
|
|
112
|
+
groups.set(key, []);
|
|
113
|
+
groups.get(key).push(e);
|
|
114
|
+
}
|
|
115
|
+
const sections = [];
|
|
116
|
+
for (const [sender, items] of groups) {
|
|
117
|
+
const lines = items.map((e) => {
|
|
118
|
+
const refPart = e.ref !== undefined ? ` (ref: ${e.ref})` : '';
|
|
119
|
+
return ` [${e.kind}] ${e.label}${refPart}`;
|
|
120
|
+
});
|
|
121
|
+
sections.push(`From ${sender} — ${items.length} update${items.length === 1 ? '' : 's'}:\n${lines.join('\n')}`);
|
|
122
|
+
}
|
|
123
|
+
return header + sections.join('\n\n');
|
|
124
|
+
}
|
|
@@ -5,4 +5,14 @@ export interface ParsedFrontmatter {
|
|
|
5
5
|
raw: string;
|
|
6
6
|
}
|
|
7
7
|
export declare function parseFrontmatter(source: string): ParsedFrontmatter;
|
|
8
|
+
export interface ParsedFrontmatterGeneric {
|
|
9
|
+
/** Raw, uncoerced key/value record from the YAML block (null when absent). */
|
|
10
|
+
data: Record<string, unknown> | null;
|
|
11
|
+
body: string;
|
|
12
|
+
raw: string;
|
|
13
|
+
}
|
|
14
|
+
/** Like parseFrontmatter but returns the raw key/value record instead of
|
|
15
|
+
* coercing to SkillFrontmatter. Used by consumers (e.g. subagents) that read
|
|
16
|
+
* fields skills don't declare, such as `tools` and `model`. */
|
|
17
|
+
export declare function parseFrontmatterGeneric(source: string): ParsedFrontmatterGeneric;
|
|
8
18
|
export declare function serializeFrontmatter(data: SkillFrontmatter): string;
|
package/dist/core/frontmatter.js
CHANGED
|
@@ -7,9 +7,30 @@ export function parseFrontmatter(source) {
|
|
|
7
7
|
}
|
|
8
8
|
const raw = match[1];
|
|
9
9
|
const body = source.slice(match[0].length);
|
|
10
|
-
return { data:
|
|
10
|
+
return { data: toSkillFrontmatter(parseYamlRecord(raw)), body, raw };
|
|
11
11
|
}
|
|
12
|
-
|
|
12
|
+
/** Like parseFrontmatter but returns the raw key/value record instead of
|
|
13
|
+
* coercing to SkillFrontmatter. Used by consumers (e.g. subagents) that read
|
|
14
|
+
* fields skills don't declare, such as `tools` and `model`. */
|
|
15
|
+
export function parseFrontmatterGeneric(source) {
|
|
16
|
+
const match = source.match(FRONTMATTER_RE);
|
|
17
|
+
if (!match) {
|
|
18
|
+
return { data: null, body: source, raw: '' };
|
|
19
|
+
}
|
|
20
|
+
const raw = match[1];
|
|
21
|
+
const body = source.slice(match[0].length);
|
|
22
|
+
return { data: parseYamlRecord(raw), body, raw };
|
|
23
|
+
}
|
|
24
|
+
function toSkillFrontmatter(out) {
|
|
25
|
+
const fm = {
|
|
26
|
+
name: typeof out.name === 'string' ? out.name : '',
|
|
27
|
+
description: typeof out.description === 'string' ? out.description : undefined,
|
|
28
|
+
keywords: Array.isArray(out.keywords) ? out.keywords : undefined,
|
|
29
|
+
type: isSkillType(out.type) ? out.type : undefined,
|
|
30
|
+
};
|
|
31
|
+
return fm;
|
|
32
|
+
}
|
|
33
|
+
function parseYamlRecord(yaml) {
|
|
13
34
|
const lines = yaml.split(/\r?\n/);
|
|
14
35
|
const out = {};
|
|
15
36
|
let i = 0;
|
|
@@ -123,13 +144,7 @@ function parseSimpleYaml(yaml) {
|
|
|
123
144
|
out[key] = stripQuotes(rest);
|
|
124
145
|
i++;
|
|
125
146
|
}
|
|
126
|
-
|
|
127
|
-
name: typeof out.name === 'string' ? out.name : '',
|
|
128
|
-
description: typeof out.description === 'string' ? out.description : undefined,
|
|
129
|
-
keywords: Array.isArray(out.keywords) ? out.keywords : undefined,
|
|
130
|
-
type: isSkillType(out.type) ? out.type : undefined,
|
|
131
|
-
};
|
|
132
|
-
return fm;
|
|
147
|
+
return out;
|
|
133
148
|
}
|
|
134
149
|
function stripQuotes(s) {
|
|
135
150
|
if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
|
package/dist/core/help.d.ts
CHANGED
|
@@ -47,17 +47,40 @@ export interface ContextFileParam {
|
|
|
47
47
|
shape?: string;
|
|
48
48
|
}
|
|
49
49
|
export type InputParam = PositionalParam | FlagParam | StdinParam | ContextFileParam;
|
|
50
|
+
/** A subtree's self-description at the parent (root) level. Each subtree owns
|
|
51
|
+
* the content that represents it one level up: its vocabulary line, its
|
|
52
|
+
* selection rubric, and any bounded block it contributes to the parent's -h.
|
|
53
|
+
* defineRoot assembles the root help from these — root never hardcodes a
|
|
54
|
+
* subtree's representation. See cli-design "Each node owns its parent-level
|
|
55
|
+
* representation". */
|
|
56
|
+
export interface RootEntry {
|
|
57
|
+
/** One-line vocabulary desc — what this subtree is. Rendered first in the
|
|
58
|
+
* subtree's <name> block at root. */
|
|
59
|
+
concept: string;
|
|
60
|
+
/** Operations summary (verb list). Carried for completeness; the root block
|
|
61
|
+
* leads with concept + rubric, so this is available but not rendered. */
|
|
62
|
+
desc: string;
|
|
63
|
+
/** The selection rubric — `use when X` in the subtree's <name> block. */
|
|
64
|
+
useWhen: string;
|
|
65
|
+
/** Optional bounded block this subtree contributes to its <name> block at
|
|
66
|
+
* root. Returns a complete self-named state element (build it with
|
|
67
|
+
* stateBlock), e.g. `<skills count="42">…</skills>`. Aggregate, never an
|
|
68
|
+
* unbounded enumeration on a cold path. Soft-fails to omission on
|
|
69
|
+
* null/throw. */
|
|
70
|
+
dynamicState?: () => string | null;
|
|
71
|
+
}
|
|
50
72
|
export interface RootHelp {
|
|
51
73
|
tagline: string;
|
|
52
|
-
/**
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
subtrees: {
|
|
74
|
+
/** One entry per listed subtree. Each renders as its own <name> XML block at
|
|
75
|
+
* root, carrying the subtree's concept, selection rubric, and any nested
|
|
76
|
+
* runtime-state block. Assembled from subtrees' RootEntry by defineRoot;
|
|
77
|
+
* root hardcodes none of it. */
|
|
78
|
+
commands: {
|
|
58
79
|
name: string;
|
|
80
|
+
concept: string;
|
|
59
81
|
desc: string;
|
|
60
82
|
useWhen: string;
|
|
83
|
+
dynamicState?: () => string | null;
|
|
61
84
|
}[];
|
|
62
85
|
globals: {
|
|
63
86
|
name: string;
|
|
@@ -69,8 +92,9 @@ export interface BranchHelp {
|
|
|
69
92
|
summary: string;
|
|
70
93
|
/** Local lifecycle/model line that extends the parent definition. */
|
|
71
94
|
model?: string;
|
|
72
|
-
/** Bounded runtime aggregate
|
|
73
|
-
*
|
|
95
|
+
/** Bounded runtime aggregate as a complete self-named state element (build
|
|
96
|
+
* it with stateBlock), e.g. `<skills count="42">…</skills>`. Renderer
|
|
97
|
+
* soft-fails to omission if this returns null or throws. */
|
|
74
98
|
dynamicState?: () => string | null;
|
|
75
99
|
children: {
|
|
76
100
|
name: string;
|
|
@@ -93,6 +117,13 @@ export interface LeafHelp {
|
|
|
93
117
|
* leaves use exactly: ["None. Read-only."] */
|
|
94
118
|
effects: string[];
|
|
95
119
|
}
|
|
120
|
+
/** Build a self-named runtime-state element: `<tag attr="v">body</tag>`. The
|
|
121
|
+
* subtree that owns the state authors it through this, so the tag name and any
|
|
122
|
+
* scalar metadata (e.g. a count) travel with the data and render identically
|
|
123
|
+
* at every level the block appears. The tag name carries the label, so the
|
|
124
|
+
* body never repeats it. Attribute values are controlled (counts, short
|
|
125
|
+
* tokens) and not escaped. */
|
|
126
|
+
export declare function stateBlock(tag: string, attrs: Record<string, string | number>, body: string): string;
|
|
96
127
|
export declare function renderRoot(h: RootHelp): string;
|
|
97
128
|
export declare function renderBranch(h: BranchHelp): string;
|
|
98
129
|
export declare function renderLeafArgv(h: LeafHelp): string;
|
package/dist/core/help.js
CHANGED
|
@@ -5,6 +5,30 @@
|
|
|
5
5
|
// ---------------------------------------------------------------------------
|
|
6
6
|
// Internal helpers
|
|
7
7
|
// ---------------------------------------------------------------------------
|
|
8
|
+
/** Build a self-named runtime-state element: `<tag attr="v">body</tag>`. The
|
|
9
|
+
* subtree that owns the state authors it through this, so the tag name and any
|
|
10
|
+
* scalar metadata (e.g. a count) travel with the data and render identically
|
|
11
|
+
* at every level the block appears. The tag name carries the label, so the
|
|
12
|
+
* body never repeats it. Attribute values are controlled (counts, short
|
|
13
|
+
* tokens) and not escaped. */
|
|
14
|
+
export function stateBlock(tag, attrs, body) {
|
|
15
|
+
const a = Object.entries(attrs)
|
|
16
|
+
.map(([k, v]) => ` ${k}="${v}"`)
|
|
17
|
+
.join('');
|
|
18
|
+
return `<${tag}${a}>\n${body}\n</${tag}>`;
|
|
19
|
+
}
|
|
20
|
+
/** Evaluate a dynamicState hook, soft-failing to null on throw or empty. */
|
|
21
|
+
function evalDynamic(fn) {
|
|
22
|
+
if (fn === undefined)
|
|
23
|
+
return null;
|
|
24
|
+
try {
|
|
25
|
+
const s = fn();
|
|
26
|
+
return s !== null && s !== '' ? s : null;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
8
32
|
/** Return the longest string length in an array of names. */
|
|
9
33
|
function maxLen(names) {
|
|
10
34
|
let max = 0;
|
|
@@ -21,29 +45,41 @@ function pad(s, width) {
|
|
|
21
45
|
// ---------------------------------------------------------------------------
|
|
22
46
|
// renderRoot
|
|
23
47
|
// ---------------------------------------------------------------------------
|
|
24
|
-
const IO_CONTRACT = 'I/O contract: flags and positional args on input
|
|
48
|
+
const IO_CONTRACT = 'I/O contract: flags and positional args on input; stdout is agent-ready markdown/XML you\n' +
|
|
49
|
+
'act on directly — read it as a continuation of your prompt, don\'t parse it as data.\n' +
|
|
25
50
|
'Exit 0 on success, non-zero on failure. Schemas appear at leaf -h.';
|
|
51
|
+
// Behavioral instruction (not a schema) — engrained in the appended system
|
|
52
|
+
// prompt so the model treats unfamiliar capabilities as a cue to discover the
|
|
53
|
+
// contract, never to guess. Lives in the root guide, outside any leaf -h.
|
|
54
|
+
const CAPABILITY_DISCOVERY = "If the user mentions or implies a crtr capability you don't fully understand, " +
|
|
55
|
+
'do not guess or assume it is unsupported — run `-h` on the relevant command ' +
|
|
56
|
+
'(append it anywhere along the path) to read the contract before acting.';
|
|
26
57
|
export function renderRoot(h) {
|
|
27
58
|
const lines = [];
|
|
28
59
|
lines.push(`${h.tagline}`);
|
|
29
60
|
lines.push('');
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
for (
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
61
|
+
// Each subtree is one <command name="…"> block. The uniform wrapper states
|
|
62
|
+
// "this is a command you invoke as `crtr <name>`" — so the model reads them
|
|
63
|
+
// by one rule, and a nested state element (which is never a <command>) can't
|
|
64
|
+
// be mistaken for a sibling command. Inside: the concept (what it is), the
|
|
65
|
+
// selection rubric (when to pick it), then any self-named state element
|
|
66
|
+
// grouped with the command it belongs to. Once injected into a system prompt,
|
|
67
|
+
// each block reads as one self-contained concern domain. Header (tagline) and
|
|
68
|
+
// footer (Globals + I/O contract + capability-discovery rule) are the only
|
|
69
|
+
// non-command areas. Two levels of nesting: <command> → <state>.
|
|
70
|
+
for (const c of h.commands) {
|
|
71
|
+
lines.push(`<command name="${c.name}">`);
|
|
72
|
+
lines.push(c.concept);
|
|
73
|
+
lines.push(`use when ${c.useWhen}`);
|
|
74
|
+
// dynamicState returns a complete self-named element (e.g.
|
|
75
|
+
// <skills count="42">…</skills>) — emit it as-is, nested in the command.
|
|
76
|
+
const state = evalDynamic(c.dynamicState);
|
|
77
|
+
if (state !== null)
|
|
78
|
+
lines.push(state);
|
|
79
|
+
lines.push('</command>');
|
|
80
|
+
lines.push('');
|
|
44
81
|
}
|
|
45
|
-
|
|
46
|
-
// Globals block
|
|
82
|
+
// Globals block (footer)
|
|
47
83
|
lines.push('Globals');
|
|
48
84
|
const gNameW = maxLen(h.globals.map((g) => g.name));
|
|
49
85
|
for (const g of h.globals) {
|
|
@@ -51,6 +87,8 @@ export function renderRoot(h) {
|
|
|
51
87
|
}
|
|
52
88
|
lines.push('');
|
|
53
89
|
lines.push(IO_CONTRACT);
|
|
90
|
+
lines.push('');
|
|
91
|
+
lines.push(CAPABILITY_DISCOVERY);
|
|
54
92
|
return lines.join('\n');
|
|
55
93
|
}
|
|
56
94
|
// ---------------------------------------------------------------------------
|
|
@@ -59,25 +97,20 @@ export function renderRoot(h) {
|
|
|
59
97
|
export function renderBranch(h) {
|
|
60
98
|
const lines = [];
|
|
61
99
|
lines.push(`${h.name}: ${h.summary}.`);
|
|
100
|
+
// Dynamic content leads — the live aggregate (e.g. the <skills> catalog)
|
|
101
|
+
// renders right after the name, before the hardcoded model prose, so current
|
|
102
|
+
// state is read first. The subtree authors the whole element, so the same
|
|
103
|
+
// self-named block appears identically at root and at `skill -h`.
|
|
104
|
+
const branchState = evalDynamic(h.dynamicState);
|
|
105
|
+
if (branchState !== null) {
|
|
106
|
+
// dynamicState returns a complete self-named element — emit as-is.
|
|
107
|
+
lines.push('');
|
|
108
|
+
lines.push(branchState);
|
|
109
|
+
}
|
|
62
110
|
if (h.model !== undefined) {
|
|
111
|
+
lines.push('');
|
|
63
112
|
lines.push(h.model);
|
|
64
113
|
}
|
|
65
|
-
// Dynamic state — soft-fail to omission. Rendered as its own block,
|
|
66
|
-
// blank-line separated from the summary, so a multi-line runtime
|
|
67
|
-
// aggregate (e.g. the loaded-skills catalog) reads cleanly.
|
|
68
|
-
if (h.dynamicState !== undefined) {
|
|
69
|
-
let state = null;
|
|
70
|
-
try {
|
|
71
|
-
state = h.dynamicState();
|
|
72
|
-
}
|
|
73
|
-
catch {
|
|
74
|
-
// soft-fail: omit the block
|
|
75
|
-
}
|
|
76
|
-
if (state !== null && state !== '') {
|
|
77
|
-
lines.push('');
|
|
78
|
-
lines.push(state);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
114
|
lines.push('');
|
|
82
115
|
lines.push('Branches');
|
|
83
116
|
const nameW = maxLen(h.children.map((c) => c.name));
|
|
@@ -148,8 +181,9 @@ export function renderLeafArgv(h) {
|
|
|
148
181
|
lines.push(h.inputNote !== undefined ? h.inputNote : 'No input parameters.');
|
|
149
182
|
}
|
|
150
183
|
lines.push('');
|
|
151
|
-
|
|
152
|
-
|
|
184
|
+
// The result is rendered as instruction-shaped XML+markdown; these fields are
|
|
185
|
+
// the information it carries, in order, not a literal JSON shape.
|
|
186
|
+
lines.push('Output (fields carried in the rendered result)');
|
|
153
187
|
const outNameW = maxLen(h.output.map((f) => f.name));
|
|
154
188
|
for (const f of h.output) {
|
|
155
189
|
lines.push(` ${pad(f.name, outNameW)} ${f.type}. ${f.constraint}`);
|
package/dist/core/io.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { CrtrError } from './errors.js';
|
|
2
2
|
import { type ExitCodeValue } from '../types.js';
|
|
3
|
+
/** Set by the dispatcher when `--json` is present anywhere in argv. */
|
|
4
|
+
export declare function setJsonOutput(v: boolean): void;
|
|
5
|
+
/** True when the caller asked for raw JSON instead of rendered prose. */
|
|
6
|
+
export declare function isJsonOutput(): boolean;
|
|
3
7
|
/** Structured error payload. `error` is a stable code the agent branches on;
|
|
4
8
|
* `next` is the recovery road sign. */
|
|
5
9
|
export interface ErrorPayload {
|
|
@@ -17,10 +21,20 @@ export declare class InputError extends CrtrError {
|
|
|
17
21
|
/** Read raw stdin to EOF. Returns empty string when stdin is a TTY (no pipe).
|
|
18
22
|
* Called by the argv parser for leaves declaring a `stdin` parameter. */
|
|
19
23
|
export declare function readStdinRaw(): Promise<string>;
|
|
20
|
-
/**
|
|
24
|
+
/** Raw-JSON mirror of a single-shot response (the `--json` escape hatch). The
|
|
25
|
+
* default path renders the result as prose instead — see render.ts. */
|
|
21
26
|
export declare function emit(obj: Record<string, unknown>): void;
|
|
22
27
|
/** One JSONL record. Call per event in a stream; partial reads stay parseable. */
|
|
23
28
|
export declare function emitLine(obj: Record<string, unknown>): void;
|
|
29
|
+
/**
|
|
30
|
+
* Write to stdout and resolve true ONLY once the bytes are confirmed flushed to
|
|
31
|
+
* a connected reader. Resolves false if the consumer is gone (EPIPE) or the
|
|
32
|
+
* write fails. This is the reliable "the caller actually received it" signal:
|
|
33
|
+
* use it to gate side effects that must only happen on genuine delivery (e.g.
|
|
34
|
+
* acking a collected result). A killed process never resolves at all — also
|
|
35
|
+
* safe, since the gated side effect then never runs.
|
|
36
|
+
*/
|
|
37
|
+
export declare function writeStdout(s: string): Promise<boolean>;
|
|
24
38
|
export declare function diag(message: string): void;
|
|
25
39
|
/** Terminal error handler. Command-level failures (bad input, not-found,
|
|
26
40
|
* ambiguous) surface as the JSON response on stdout so the caller parses one
|