@crouton-kit/crouter 0.3.48 → 0.3.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/builtin-pi-packages/pi-crtr-extensions/README.md +1 -1
- package/dist/clients/attach/attach-cmd.js +825 -863
- package/dist/clients/attach/titled-editor.js +6 -3
- package/dist/commands/human/prompts.js +1 -1
- package/dist/commands/human/shared.js +7 -3
- package/dist/commands/memory/write.js +2 -0
- package/dist/commands/node.js +96 -11
- package/dist/commands/profile/default.d.ts +2 -0
- package/dist/commands/profile/default.js +143 -0
- package/dist/commands/profile/new.js +3 -3
- package/dist/commands/profile/project.d.ts +2 -0
- package/dist/commands/profile/project.js +97 -0
- package/dist/commands/profile/show.js +1 -1
- package/dist/commands/profile.js +10 -11
- package/dist/commands/sys/__tests__/sync-import.test.js +156 -3
- package/dist/commands/sys/sync.js +82 -25
- package/dist/core/__tests__/broker-sdk-wiring.test.js +4 -7
- package/dist/core/__tests__/context-intro.test.js +15 -7
- package/dist/core/__tests__/memory-resolver-precedence.test.d.ts +1 -0
- package/dist/core/__tests__/memory-resolver-precedence.test.js +144 -0
- package/dist/core/__tests__/on-read-identity.test.d.ts +1 -0
- package/dist/core/__tests__/on-read-identity.test.js +68 -0
- package/dist/core/fs-utils.d.ts +1 -1
- package/dist/core/fs-utils.js +5 -3
- package/dist/core/memory-resolver.d.ts +27 -11
- package/dist/core/memory-resolver.js +105 -109
- package/dist/core/profiles/default-binding.d.ts +10 -0
- package/dist/core/profiles/default-binding.js +50 -0
- package/dist/core/profiles/select.d.ts +13 -1
- package/dist/core/profiles/select.js +92 -26
- package/dist/core/runtime/bearings.d.ts +4 -9
- package/dist/core/runtime/bearings.js +10 -17
- package/dist/core/runtime/front-door.js +11 -4
- package/dist/core/runtime/revive.js +2 -1
- package/dist/core/runtime/spawn.d.ts +4 -0
- package/dist/core/runtime/spawn.js +1 -1
- package/dist/core/substrate/index.d.ts +1 -1
- package/dist/core/substrate/index.js +3 -1
- package/dist/core/substrate/on-read.js +14 -9
- package/dist/core/substrate/render.js +7 -3
- package/dist/core/substrate/schema.d.ts +8 -2
- package/dist/core/substrate/schema.js +19 -2
- package/dist/daemon/crtrd.js +44 -2
- package/dist/daemon/manage.d.ts +20 -0
- package/dist/daemon/manage.js +64 -2
- package/package.json +3 -3
- package/dist/commands/profile/add-project.d.ts +0 -1
- package/dist/commands/profile/add-project.js +0 -42
- package/dist/commands/profile/remove-project.d.ts +0 -1
- package/dist/commands/profile/remove-project.js +0 -42
|
@@ -10,9 +10,12 @@ import { truncateToWidth, visibleWidth, } from '@earendil-works/pi-tui';
|
|
|
10
10
|
/** The thinking color scale is a deliberate cool→warm ramp between a blue and
|
|
11
11
|
* its orange complement, evenly spaced across the five active levels so each
|
|
12
12
|
* step stays readable without jumping through unrelated hues. `off` falls
|
|
13
|
-
* back to the caller's default border color.
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
* back to the caller's default border color. The endpoints are deliberately
|
|
14
|
+
* deep (not bright) hues: the same colors double as the title-chip BACKGROUND
|
|
15
|
+
* under bold white text, so every step must clear ~4.5:1 white-on-color
|
|
16
|
+
* contrast — a lighter blue/orange washes the label out. */
|
|
17
|
+
const THINKING_START = [46, 92, 190]; // deep blue
|
|
18
|
+
const THINKING_END = [166, 98, 24]; // deep amber
|
|
16
19
|
function interpolateRgb(start, end, t) {
|
|
17
20
|
return [
|
|
18
21
|
Math.round(start[0] + (end[0] - start[0]) * t),
|
|
@@ -36,7 +36,7 @@ export const humanAsk = defineLeaf({
|
|
|
36
36
|
help: {
|
|
37
37
|
name: 'human ask',
|
|
38
38
|
summary: 'put a humanloop decision deck in front of a person; returns a job handle immediately. This is the default, expected channel for posing ANY question or decision to the user — reach for it instead of writing the question as prose in your reply.',
|
|
39
|
-
guide: 'Use exactly one of these forms: `crtr human ask "<question>"` for a simple open-text prompt (the CLI builds a one-item humanloop deck internally), or `crtr human ask --context-file deck.json` for a fully structured deck. The kickoff is instant and NEVER blocks — "never block on the result" refers only to not busy-waiting on the job; the human answering on their own time is not a reason to avoid asking or to fall back to inline prose. The deck body is directive-flavored markdown rendered by termrender (panels, columns, trees, callouts, mermaid) — see `termrender doc -h` for the directive set before authoring one.',
|
|
39
|
+
guide: 'Use exactly one of these forms: `crtr human ask "<question>"` for a simple open-text prompt (the CLI builds a one-item humanloop deck internally), or `crtr human ask --context-file deck.json` for a fully structured deck. The kickoff is instant and NEVER blocks — "never block on the result" refers only to not busy-waiting on the job; the human answering on their own time is not a reason to avoid asking or to fall back to inline prose. The deck body is directive-flavored markdown rendered by termrender (panels, columns, trees, callouts, mermaid) — see `termrender doc -h` for the directive set before authoring one. Author a GOOD deck: ask only what materially changes your next step (each question spends the human\'s attention), offer genuine alternatives as `options` (not one real pick plus filler), and layer content by progressive disclosure — `title` a short topic, `subtitle` the one-sentence TL;DR, `body` the plain-language detail with denser material under a `## Details` heading the reader can skip.',
|
|
40
40
|
params: [
|
|
41
41
|
{ kind: 'positional', name: 'question', type: 'string', required: false, constraint: 'Bare-question shorthand for a simple open-text ask. Mutually exclusive with `--context-file`.' },
|
|
42
42
|
{ kind: 'context-file', name: 'deck', required: false, constraint: 'Strict complex-ask deck JSON. Use this when you need multiple interactions, options, or other deck structure. Mutually exclusive with the question shorthand.', shape: DECK_SCHEMA_HINT },
|
|
@@ -5,9 +5,13 @@ import { atomicWriteJson, readJson } from '@crouton-kit/humanloop';
|
|
|
5
5
|
import { countPanesInWindow, spawnAndDetach, shellQuote, attachedClientPane, paneAlive, } from '../../core/spawn.js';
|
|
6
6
|
import { graphSurfaceTarget } from '../../core/runtime/placement.js';
|
|
7
7
|
export const DECK_SCHEMA_HINT = 'Deck must match the humanloop deck schema: {title?, ' +
|
|
8
|
-
'source?:{sessionName?,askedBy?,blockedSince?}, ' +
|
|
9
|
-
'
|
|
10
|
-
'
|
|
8
|
+
'source?:{sessionName?,askedBy?,blockedSince?}, interactions:[{id, ' +
|
|
9
|
+
'title (short ≤~4-word topic — the thing being decided, not the decision), ' +
|
|
10
|
+
'subtitle? (ONE plain-English sentence — the TL;DR/stakes, not a paragraph), ' +
|
|
11
|
+
'(body?|bodyPath?) (the full explanation and the ONLY place for long or rich ' +
|
|
12
|
+
'prose — directive-flavored markdown rendered by termrender; never put the ' +
|
|
13
|
+
'wall of detail in subtitle), options:[{id,label,description?}], multiSelect?, ' +
|
|
14
|
+
'allowFreetext?, freetextLabel?, ' +
|
|
11
15
|
"kind?:'notify'|'decision'|'context'|'error'}]}.";
|
|
12
16
|
export function resolveMaxPanes() {
|
|
13
17
|
return readConfig('user').max_panes_per_window;
|
|
@@ -12,7 +12,9 @@ export const writeLeaf = defineLeaf({
|
|
|
12
12
|
summary: 'create or update memory/<name>.md at the resolved scope from frontmatter flags + a stdin body',
|
|
13
13
|
guide: 'The body is the easy part; the craft is routing — every frontmatter flag decides who sees this doc, when, and at what context cost. Each rung up is paid by every future agent at every boot or read, forever, so default each rung down.\n\n' +
|
|
14
14
|
'Pick the kind. knowledge is consulted for facts or procedures; preference directs behavior. The kind choice is about how the doc is used, not about how long it is.\n\n' +
|
|
15
|
+
'Store reusable current truth, not session notes. Useful memories are non-obvious procedures, gotchas, durable preferences, cross-repo conventions, and amendments to plans/specs that future agents must honor. Put plan/spec amendments under path names like `projects/<topic>/...`; add `projects/<topic>/INDEX.md` with `name` visibility so the topic is discoverable without loading the whole body. Do not store chat summaries, implementation history, or facts already recorded in the repo.\n\n' +
|
|
15
16
|
'Set both visibility rungs explicitly on create. There is no kind default. `none`, `name`, `preview`, and `content` move from least to most loaded: `none` keeps the doc out of auto-load and on-read surfaces, `name` is the bare doc tag only, `preview` is the name + envelope + routing line (`when-and-why-to-read`), and `content` inlines the whole body when the body is short enough to justify it. Each axis is independent; usually one carries a real rung and the other is `none`. When a doc is short enough to state in a single sentence — a one-line preference, or a knowledge fact that fits in a sentence — skip `preview` and use `name` or `content` directly: the routing line would run longer than the doc itself, so a `preview` rung just adds words. Sentence-length `content` docs are not just acceptable, they are correct. Never pad a memory to be more verbose than the rule or fact it carries.\n\n' +
|
|
17
|
+
'Choose the scope. `project` is for facts any agent in one repo needs. `user` is for person-wide facts and preferences that should follow the user everywhere. `profile` is for the profile’s bundle of dirs: cross-repo conventions, how the pieces relate, or the user’s stance toward that body of work. When unsure, choose the narrowest scope that will still reach the next agent who needs it.\n\n' +
|
|
16
18
|
'Choose the hook — boot vs file-read. Behavior and how-to procedure surface at boot. Knowledge about code belongs next to the code: put the file in that directory’s .crouter/memory/ (`--dir <project-dir>` targets it exactly, from anywhere) and it fires positionally when files there are read, costing nothing at boot. A knowledge doc about a person or process has no code directory to anchor to, so on-read triggering is meaningless — route it through boot instead and set the boot rung to `preview` when you want the routing line to surface.\n\n' +
|
|
17
19
|
'Write the routing line (--when-and-why-to-read) first, before storing anything: "When <circumstance>, this <kind> should be read because <payoff>." The test is whether a stranger mid-task can decide from that one line alone whether to spend the read. If you cannot name the concrete situation that triggers it, you do not yet understand the memory — ask the user one sharp question instead of improvising. Never paraphrase the advice in the routing line; keep it about when to read and why the read is worth it.\n\n' +
|
|
18
20
|
'Gate and read-when share the same predicate language: a field map is AND-ed across fields; dotted fields resolve nested values; field matchers may be scalar, array, or object. Scalar matchers do exact equality, with arrays matching any element. Array matchers do membership or intersection. Object matchers accept `eq`, `ne`, `in`, `nin`, `exists`, `contains`, `containsAll`, `containsAny`, `matches`, `imatches`, `gt`, `gte`, `lt`, and `lte`. Combinators are `all`, `any`, and `not`; sibling field matchers next to them are AND-ed in. An empty condition is inert. An unknown op never matches.\n\n' +
|
package/dist/commands/node.js
CHANGED
|
@@ -22,6 +22,7 @@ import { nodeContextLeaf } from './node-context.js';
|
|
|
22
22
|
import { nodeArtifactsLeaf } from './node-inspect-artifacts.js';
|
|
23
23
|
import { nodeWorktreeBranch } from './node-worktree.js';
|
|
24
24
|
import { nodeReviveLeaf } from './node-lifecycle-revive.js';
|
|
25
|
+
import { isPidAlive } from '../core/canvas/pid.js';
|
|
25
26
|
import { recycleNode } from '../core/runtime/recycle.js';
|
|
26
27
|
import { readFaultAsync } from '../core/runtime/fault.js';
|
|
27
28
|
import { activeFaultForDisplay } from '../core/canvas/render.js';
|
|
@@ -34,7 +35,7 @@ import { appendInbox } from '../core/feed/inbox.js';
|
|
|
34
35
|
import { readMergedLaunchConfig } from '../core/config.js';
|
|
35
36
|
import { listProfiles } from '../core/profiles/manifest.js';
|
|
36
37
|
import { stateBlock } from '../core/help.js';
|
|
37
|
-
import { getNode, updateNode, listNodes, subscribe, unsubscribe, subscriptionsOf, subscribersOf, readContextTokens, view, armTrigger, listTriggers, cancelTrigger, TriggerArmError, fullName, } from '../core/canvas/index.js';
|
|
38
|
+
import { getNode, updateNode, listNodes, subscribe, unsubscribe, subscriptionsOf, subscribersOf, contextDir, reportsDir, nodeArtifacts, readContextTokens, view, armTrigger, listTriggers, cancelTrigger, TriggerArmError, fullName, } from '../core/canvas/index.js';
|
|
38
39
|
// Past this much context, an ORCHESTRATOR that spawns a managed child is better
|
|
39
40
|
// off yielding than holding its fat window open for the child's result: the
|
|
40
41
|
// child's finish revives it fresh against its roadmap, so a clean window absorbs
|
|
@@ -436,26 +437,42 @@ const nodeList = defineLeaf({
|
|
|
436
437
|
// ---------------------------------------------------------------------------
|
|
437
438
|
// node inspect show — a node + its place in the spine
|
|
438
439
|
// ---------------------------------------------------------------------------
|
|
440
|
+
// Enrich a bare subscription edge with the neighbor's name/kind/status so a
|
|
441
|
+
// debugger reads who a report/manager IS without a second `show` per id.
|
|
442
|
+
function enrichNeighbor(ref) {
|
|
443
|
+
const n = getNode(ref.node_id);
|
|
444
|
+
return {
|
|
445
|
+
node_id: ref.node_id,
|
|
446
|
+
name: n ? fullName(n) : '(missing)',
|
|
447
|
+
kind: n?.kind ?? '?',
|
|
448
|
+
status: n ? (n.intent ? `${n.status}/${n.intent}` : n.status) : '?',
|
|
449
|
+
active: ref.active,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
439
452
|
const nodeShow = defineLeaf({
|
|
440
453
|
name: 'show',
|
|
441
|
-
description: 'show a node + its spine neighbors',
|
|
442
|
-
whenToUse: 'you want one node in depth:
|
|
454
|
+
description: 'show a node + its spine neighbors, artifacts, and drill-in pointers',
|
|
455
|
+
whenToUse: 'you want one node in depth — the debugging entrypoint: a glanceable meta slice (status/intent, liveness, cwd, session file), its named spine neighbors (reports down, managers up), how many artifacts/triggers it has, and the exact next commands + on-disk paths to read its context/reports dirs and children. Use `node inspect list` instead for the flat roster of every node, or `canvas dashboard` to see the whole-tree shape',
|
|
443
456
|
help: {
|
|
444
457
|
name: 'node inspect show',
|
|
445
|
-
summary: 'show a node
|
|
458
|
+
summary: 'show a node in depth — meta slice, named neighbors (reports/managers), artifact/trigger counts, and drill-in pointers (context dir, session file, children). The debugging entrypoint for one node.',
|
|
446
459
|
params: [
|
|
447
460
|
{ kind: 'positional', name: 'node', required: true, constraint: 'Node id.' },
|
|
448
461
|
],
|
|
449
462
|
output: [
|
|
450
|
-
{ name: 'node', type: 'object', required: true, constraint: 'The node meta.' },
|
|
451
|
-
{ name: 'reports', type: 'object[]', required: true, constraint: 'Who this node subscribes to (its reports/down).' },
|
|
452
|
-
{ name: 'managers', type: 'object[]', required: true, constraint: 'Who subscribes to this node (its managers/up).' },
|
|
463
|
+
{ name: 'node', type: 'object', required: true, constraint: 'The full node meta (system prompt + every telemetry field). The render surfaces a glanceable slice; `--json` carries the complete dump.' },
|
|
464
|
+
{ name: 'reports', type: 'object[]', required: true, constraint: 'Who this node subscribes to (its reports/down), each enriched with {node_id, name, kind, status, active} — these are typically its spawned children.' },
|
|
465
|
+
{ name: 'managers', type: 'object[]', required: true, constraint: 'Who subscribes to this node (its managers/up), same enriched shape.' },
|
|
466
|
+
{ name: 'artifacts', type: 'object', required: true, constraint: 'Counts of what the node left on disk by corpus: {report, doc, roadmap}. Enumerate them with `node inspect artifacts <id>`.' },
|
|
467
|
+
{ name: 'pending_triggers', type: 'number', required: true, constraint: 'How many clock/predicate triggers this node has armed. Inspect with `node triggers list --node <id>`.' },
|
|
468
|
+
{ name: 'paths', type: 'object', required: true, constraint: 'Resolved on-disk locations: {context_dir, reports_dir, session_file} — read these directly to debug.' },
|
|
469
|
+
{ name: 'follow_up', type: 'string', required: true, constraint: 'The concrete next commands to drill deeper.' },
|
|
453
470
|
],
|
|
454
471
|
outputKind: 'object',
|
|
455
|
-
effects: ['Read-only: reads the node meta
|
|
472
|
+
effects: ['Read-only: reads the node meta, canvas.db edges, on-disk artifacts, and armed triggers.'],
|
|
456
473
|
// The node meta object inlines the full system prompt + every telemetry
|
|
457
|
-
// field — a machine-readable dump
|
|
458
|
-
//
|
|
474
|
+
// field — a machine-readable dump we suppress from the viewer preview; the
|
|
475
|
+
// render below surfaces only a glanceable slice. Same preview rationale as
|
|
459
476
|
// canvas snapshot / node inspect snapshot.
|
|
460
477
|
preview: { suppressOutput: true },
|
|
461
478
|
},
|
|
@@ -465,7 +482,75 @@ const nodeShow = defineLeaf({
|
|
|
465
482
|
if (node === null) {
|
|
466
483
|
throw new InputError({ error: 'not_found', message: `no node: ${id}`, next: 'List nodes with `crtr node inspect list`.' });
|
|
467
484
|
}
|
|
468
|
-
|
|
485
|
+
const reports = subscriptionsOf(id).map(enrichNeighbor);
|
|
486
|
+
const managers = subscribersOf(id).map(enrichNeighbor);
|
|
487
|
+
const arts = nodeArtifacts(id, ['report', 'doc', 'roadmap']);
|
|
488
|
+
const artifacts = {
|
|
489
|
+
report: arts.filter((a) => a.source === 'report').length,
|
|
490
|
+
doc: arts.filter((a) => a.source === 'doc').length,
|
|
491
|
+
roadmap: arts.filter((a) => a.source === 'roadmap').length,
|
|
492
|
+
};
|
|
493
|
+
const pending_triggers = listTriggers({ node: id }).length;
|
|
494
|
+
const paths = {
|
|
495
|
+
context_dir: contextDir(id),
|
|
496
|
+
reports_dir: reportsDir(id),
|
|
497
|
+
session_file: node.pi_session_file ?? null,
|
|
498
|
+
};
|
|
499
|
+
// Auto-gathered drill-in pointers: the exact commands a debugger reaches for
|
|
500
|
+
// next, populated only when there is actually something behind them.
|
|
501
|
+
const hints = [];
|
|
502
|
+
const artTotal = artifacts.report + artifacts.doc + artifacts.roadmap;
|
|
503
|
+
if (artTotal > 0)
|
|
504
|
+
hints.push(`Read its ${artTotal} artifact(s): \`crtr node inspect artifacts ${id}\` → each ref feeds \`crtr canvas history read <ref>\`.`);
|
|
505
|
+
hints.push(`Browse its context + reports dirs (and its subscriptions') in nvim: \`crtr node inspect context ${id}\`.`);
|
|
506
|
+
if (paths.session_file)
|
|
507
|
+
hints.push(`Read the raw conversation transcript: ${paths.session_file}`);
|
|
508
|
+
hints.push(`On-disk context dir (ls/read directly): ${paths.context_dir}`);
|
|
509
|
+
if (pending_triggers > 0)
|
|
510
|
+
hints.push(`It has ${pending_triggers} armed trigger(s): \`crtr node triggers list --node ${id}\`.`);
|
|
511
|
+
if (reports.length > 0)
|
|
512
|
+
hints.push(`Drill into a child/report: \`crtr node inspect show <node_id>\` (${reports.map((r) => r['node_id']).join(', ')}).`);
|
|
513
|
+
hints.push(`Full machine dump (system prompt + all telemetry): \`crtr --json node inspect show ${id}\`.`);
|
|
514
|
+
return { node, reports, managers, artifacts, pending_triggers, paths, follow_up: hints.join('\n') };
|
|
515
|
+
},
|
|
516
|
+
render: (r) => {
|
|
517
|
+
const node = r['node'];
|
|
518
|
+
const id = String(node['node_id']);
|
|
519
|
+
const pid = node['pi_pid'];
|
|
520
|
+
const alive = isPidAlive(pid) ? 'alive' : 'dead';
|
|
521
|
+
const intent = node['intent'] ? `/${String(node['intent'])}` : '';
|
|
522
|
+
const parts = [];
|
|
523
|
+
// 1) Glanceable meta slice — the debug-critical fields, not the full dump.
|
|
524
|
+
parts.push(`# ${fullName(node)} (${id})`);
|
|
525
|
+
const meta = [
|
|
526
|
+
`- kind/mode/lifecycle: ${node['kind']} / ${node['mode']} / ${node['lifecycle']}`,
|
|
527
|
+
`- status: ${node['status']}${intent}`,
|
|
528
|
+
`- broker pid: ${pid ?? '(none)'} — ${alive}`,
|
|
529
|
+
`- cwd: ${node['cwd']}`,
|
|
530
|
+
`- host: ${node['host_kind']}` + (node['profile_id'] ? ` · profile ${node['profile_id']}` : ''),
|
|
531
|
+
`- cycles: ${node['cycles']} · created ${node['created']}`,
|
|
532
|
+
];
|
|
533
|
+
if (node['parent'])
|
|
534
|
+
meta.push(`- parent: ${node['parent']}`);
|
|
535
|
+
parts.push(meta.join('\n'));
|
|
536
|
+
// 2) Neighbor tables (reports down, managers up), pre-enriched with names.
|
|
537
|
+
const table = (label, rows) => {
|
|
538
|
+
if (rows.length === 0)
|
|
539
|
+
return `${label}: none`;
|
|
540
|
+
const cols = ['node_id', 'name', 'kind', 'status', 'active'];
|
|
541
|
+
const head = `| ${cols.join(' | ')} |`;
|
|
542
|
+
const sep = `| ${cols.map(() => '---').join(' | ')} |`;
|
|
543
|
+
const body = rows.map((row) => `| ${cols.map((c) => String(row[c] ?? '').replace(/\|/g, '\\|').replace(/\n/g, ' ')).join(' | ')} |`).join('\n');
|
|
544
|
+
return `${rows.length} ${label}:\n\n${head}\n${sep}\n${body}`;
|
|
545
|
+
};
|
|
546
|
+
parts.push(table('reports (down — subscriptions, usually children)', r['reports']));
|
|
547
|
+
parts.push(table('managers (up — subscribers)', r['managers']));
|
|
548
|
+
// 3) On-disk footprint at a glance.
|
|
549
|
+
const a = r['artifacts'];
|
|
550
|
+
parts.push(`artifacts: ${a['report']} report(s), ${a['doc']} doc(s), ${a['roadmap']} roadmap · ${r['pending_triggers']} pending trigger(s)`);
|
|
551
|
+
// 4) Auto-gathered drill-in pointers.
|
|
552
|
+
parts.push(`Drill deeper:\n${String(r['follow_up'])}`);
|
|
553
|
+
return parts.join('\n\n');
|
|
469
554
|
},
|
|
470
555
|
});
|
|
471
556
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// `crtr profile default` — the per-cwd default-profile pin. A pin says "when a
|
|
2
|
+
// node boots in THIS directory, use THIS profile" and stops the startup
|
|
3
|
+
// chooser from asking. It is directory-scoped, not manifest state: independent
|
|
4
|
+
// of a profile's project purview (`crtr profile project`). The startup menu's
|
|
5
|
+
// `d` action writes the same pin; these verbs are the explicit surface for it.
|
|
6
|
+
import { defineLeaf, defineBranch } from '../../core/command.js';
|
|
7
|
+
import { InputError } from '../../core/io.js';
|
|
8
|
+
import { loadProfileManifest } from '../../core/profiles/manifest.js';
|
|
9
|
+
import { profileCoversCwd } from '../../core/profiles/select.js';
|
|
10
|
+
import { getDefaultProfileId, setDefaultProfileId, clearDefaultProfile, resolveBindingCwd, } from '../../core/profiles/default-binding.js';
|
|
11
|
+
const setLeaf = defineLeaf({
|
|
12
|
+
name: 'set',
|
|
13
|
+
description: 'pin a profile as the default for the current directory',
|
|
14
|
+
whenToUse: 'you want crtr to stop asking which profile to use here and always default to a chosen one — the command form of the startup menu\u2019s `d` action. The profile must already cover this directory (add it with `crtr profile project add` first if not).',
|
|
15
|
+
help: {
|
|
16
|
+
name: 'profile default set',
|
|
17
|
+
summary: "pin a profile as the current directory's default, so the startup chooser stops asking",
|
|
18
|
+
params: [
|
|
19
|
+
{
|
|
20
|
+
kind: 'positional',
|
|
21
|
+
name: 'profile',
|
|
22
|
+
required: true,
|
|
23
|
+
constraint: 'Exact profile id, or a unique manifest name. Must already cover the current directory (be at/above/below it in its project purview).',
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
output: [
|
|
27
|
+
{ name: 'dir', type: 'string', required: true, constraint: "The resolved directory the pin was set for." },
|
|
28
|
+
{ name: 'profile_id', type: 'string', required: true, constraint: 'The pinned profile id.' },
|
|
29
|
+
{ name: 'name', type: 'string', required: true, constraint: 'The pinned profile name.' },
|
|
30
|
+
{ name: 'follow_up', type: 'string', required: true, constraint: 'Concrete next commands.' },
|
|
31
|
+
],
|
|
32
|
+
outputKind: 'object',
|
|
33
|
+
effects: ['Writes `default-profile.json` in the per-cwd workspace root. The next launch here auto-picks this profile with no prompt (override once with `crtr --pick-profile`).'],
|
|
34
|
+
},
|
|
35
|
+
run: async (_input) => {
|
|
36
|
+
const cwd = process.cwd();
|
|
37
|
+
const resolved = resolveBindingCwd(cwd);
|
|
38
|
+
const entry = loadProfileManifest(_input['profile']);
|
|
39
|
+
if (!profileCoversCwd(entry, resolved)) {
|
|
40
|
+
throw new InputError({
|
|
41
|
+
error: 'not_covered',
|
|
42
|
+
message: `profile "${entry.manifest.name}" does not cover ${resolved}, so a default pinned here would be ignored.`,
|
|
43
|
+
next: `Widen its purview first: \`crtr profile project add ${entry.profileId} --dir ${resolved}\`, then re-run this.`,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
setDefaultProfileId(cwd, entry.profileId);
|
|
47
|
+
return {
|
|
48
|
+
dir: resolved,
|
|
49
|
+
profile_id: entry.profileId,
|
|
50
|
+
name: entry.manifest.name,
|
|
51
|
+
follow_up: `Launches here now auto-pick "${entry.manifest.name}". Clear with \`crtr profile default clear\`, or reopen the chooser once with \`crtr --pick-profile\`.`,
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
const clearLeaf = defineLeaf({
|
|
56
|
+
name: 'clear',
|
|
57
|
+
description: "remove the current directory's default-profile pin",
|
|
58
|
+
whenToUse: 'you no longer want a fixed default here — go back to the normal startup chooser (MRU / covering menu). No-op if nothing was pinned.',
|
|
59
|
+
help: {
|
|
60
|
+
name: 'profile default clear',
|
|
61
|
+
summary: "remove the current directory's default-profile pin",
|
|
62
|
+
params: [],
|
|
63
|
+
output: [
|
|
64
|
+
{ name: 'dir', type: 'string', required: true, constraint: 'The resolved directory the pin was cleared for.' },
|
|
65
|
+
{ name: 'cleared', type: 'boolean', required: true, constraint: 'True when a pin existed and was removed; false when nothing was pinned.' },
|
|
66
|
+
{ name: 'follow_up', type: 'string', required: true, constraint: 'Concrete next commands.' },
|
|
67
|
+
],
|
|
68
|
+
outputKind: 'object',
|
|
69
|
+
effects: ['Deletes `default-profile.json` from the per-cwd workspace root, if present.'],
|
|
70
|
+
},
|
|
71
|
+
run: async (_input) => {
|
|
72
|
+
const cwd = process.cwd();
|
|
73
|
+
const resolved = resolveBindingCwd(cwd);
|
|
74
|
+
const had = getDefaultProfileId(cwd) !== null;
|
|
75
|
+
clearDefaultProfile(cwd);
|
|
76
|
+
return {
|
|
77
|
+
dir: resolved,
|
|
78
|
+
cleared: had,
|
|
79
|
+
follow_up: had
|
|
80
|
+
? 'The startup chooser will ask again here. Re-pin with `crtr profile default set <profile>`.'
|
|
81
|
+
: 'Nothing was pinned here.',
|
|
82
|
+
};
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
const showLeaf = defineLeaf({
|
|
86
|
+
name: 'show',
|
|
87
|
+
description: "show the current directory's default-profile pin",
|
|
88
|
+
whenToUse: "you want to know which profile (if any) is pinned as this directory's default.",
|
|
89
|
+
help: {
|
|
90
|
+
name: 'profile default show',
|
|
91
|
+
summary: "report the current directory's pinned default profile, or none",
|
|
92
|
+
params: [],
|
|
93
|
+
output: [
|
|
94
|
+
{ name: 'dir', type: 'string', required: true, constraint: 'The resolved directory queried.' },
|
|
95
|
+
{ name: 'pinned', type: 'boolean', required: true, constraint: 'True when a default is pinned here.' },
|
|
96
|
+
{ name: 'profile_id', type: 'string', required: false, constraint: 'The pinned profile id, when pinned.' },
|
|
97
|
+
{ name: 'name', type: 'string', required: false, constraint: 'The pinned profile name, when it still resolves.' },
|
|
98
|
+
{ name: 'stale', type: 'boolean', required: false, constraint: 'True when a pin exists but its profile no longer resolves (deleted); the selector will self-heal it on next launch.' },
|
|
99
|
+
{ name: 'follow_up', type: 'string', required: true, constraint: 'Concrete next commands.' },
|
|
100
|
+
],
|
|
101
|
+
outputKind: 'object',
|
|
102
|
+
effects: ['Read-only.'],
|
|
103
|
+
},
|
|
104
|
+
run: async (_input) => {
|
|
105
|
+
const cwd = process.cwd();
|
|
106
|
+
const resolved = resolveBindingCwd(cwd);
|
|
107
|
+
const id = getDefaultProfileId(cwd);
|
|
108
|
+
if (id === null) {
|
|
109
|
+
return {
|
|
110
|
+
dir: resolved,
|
|
111
|
+
pinned: false,
|
|
112
|
+
follow_up: 'Pin one with `crtr profile default set <profile>`.',
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
let name;
|
|
116
|
+
try {
|
|
117
|
+
name = loadProfileManifest(id).manifest.name;
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
name = undefined;
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
dir: resolved,
|
|
124
|
+
pinned: true,
|
|
125
|
+
profile_id: id,
|
|
126
|
+
...(name !== undefined ? { name } : { stale: true }),
|
|
127
|
+
follow_up: name !== undefined
|
|
128
|
+
? 'Change it with `crtr profile default set <profile>`, or clear it with `crtr profile default clear`.'
|
|
129
|
+
: 'This profile no longer exists; clear the stale pin with `crtr profile default clear`.',
|
|
130
|
+
};
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
export const defaultBranch = defineBranch({
|
|
134
|
+
name: 'default',
|
|
135
|
+
description: "manage this directory's default-profile pin",
|
|
136
|
+
whenToUse: "setting, clearing, or inspecting which profile a directory defaults to at startup — so crtr stops asking. Directory-scoped, independent of a profile's project purview.",
|
|
137
|
+
help: {
|
|
138
|
+
name: 'profile default',
|
|
139
|
+
summary: "the per-directory default profile — pin one so the startup chooser stops asking",
|
|
140
|
+
model: "A pin binds the CURRENT directory to a profile: launches here auto-pick it with no prompt (`crtr --pick-profile` reopens the chooser once). `set` pins a profile (which must already cover this dir), `clear` removes the pin, `show` reports it. This is directory-scoped selection, NOT manifest state — independent of `crtr profile project` (a profile's purview). The startup menu's `d` action writes the same pin.",
|
|
141
|
+
},
|
|
142
|
+
children: [setLeaf, clearLeaf, showLeaf],
|
|
143
|
+
});
|
|
@@ -3,7 +3,7 @@ import { createProfile, profileRoot } from '../../core/profiles/manifest.js';
|
|
|
3
3
|
export const newLeaf = defineLeaf({
|
|
4
4
|
name: 'new',
|
|
5
5
|
description: 'create a new profile',
|
|
6
|
-
whenToUse: 'you are establishing a new agent identity — a stable id, a display name, and its own memory store. Pass at most one initial project directory here (the parser has no repeatable-flag support); use `profile add
|
|
6
|
+
whenToUse: 'you are establishing a new agent identity — a stable id, a display name, and its own memory store. Pass at most one initial project directory here (the parser has no repeatable-flag support); use `profile project add` afterward for every additional one.',
|
|
7
7
|
help: {
|
|
8
8
|
name: 'profile new',
|
|
9
9
|
summary: 'create a profile: `<profiles-root>/<slug>-<id>/` with a manifest + its own `memory/` store',
|
|
@@ -20,7 +20,7 @@ export const newLeaf = defineLeaf({
|
|
|
20
20
|
name: 'project',
|
|
21
21
|
type: 'path',
|
|
22
22
|
required: false,
|
|
23
|
-
constraint: 'One initial project directory. Must already exist and be a directory; resolved to its absolute real path before storing. Singular — add more with `crtr profile add
|
|
23
|
+
constraint: 'One initial project directory. Must already exist and be a directory; resolved to its absolute real path before storing. Singular — add more with `crtr profile project add` after creation.',
|
|
24
24
|
},
|
|
25
25
|
],
|
|
26
26
|
output: [
|
|
@@ -44,7 +44,7 @@ export const newLeaf = defineLeaf({
|
|
|
44
44
|
path: profileRoot(profileId),
|
|
45
45
|
projects: manifest.projects,
|
|
46
46
|
created_at: manifest.created_at,
|
|
47
|
-
follow_up: `Show it with \`crtr profile show ${profileId}\`, add another project with \`crtr profile
|
|
47
|
+
follow_up: `Show it with \`crtr profile show ${profileId}\`, add another project with \`crtr profile project add ${profileId} --dir <path>\`, or list every profile with \`crtr profile list\`.`,
|
|
48
48
|
};
|
|
49
49
|
},
|
|
50
50
|
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// `crtr profile project` — the project-purview sub-noun of `profile`. A
|
|
2
|
+
// profile's `projects` list is what it can SEE (memory + config resolution).
|
|
3
|
+
// These two verbs widen or narrow that list after creation; `crtr profile
|
|
4
|
+
// show` reads it back.
|
|
5
|
+
import { defineLeaf, defineBranch } from '../../core/command.js';
|
|
6
|
+
import { addProfileProject, removeProfileProject, loadProfileManifest } from '../../core/profiles/manifest.js';
|
|
7
|
+
const addLeaf = defineLeaf({
|
|
8
|
+
name: 'add',
|
|
9
|
+
description: "add a project directory to a profile's purview",
|
|
10
|
+
whenToUse: "a profile needs to see (memory + config from) another project directory — extend its purview after creation, including mid-session from a node already running under that profile.",
|
|
11
|
+
help: {
|
|
12
|
+
name: 'profile project add',
|
|
13
|
+
summary: "append a project directory to a profile's manifest, deduped by real path",
|
|
14
|
+
params: [
|
|
15
|
+
{
|
|
16
|
+
kind: 'positional',
|
|
17
|
+
name: 'profile',
|
|
18
|
+
required: true,
|
|
19
|
+
constraint: 'Exact profile id, or a unique manifest name.',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
kind: 'flag',
|
|
23
|
+
name: 'dir',
|
|
24
|
+
type: 'path',
|
|
25
|
+
required: true,
|
|
26
|
+
constraint: 'Directory to add. Must exist and be a directory; resolved to its absolute real path before storing. A path already on the manifest is a no-op, not an error.',
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
output: [
|
|
30
|
+
{ name: 'profile_id', type: 'string', required: true, constraint: 'Stable directory id.' },
|
|
31
|
+
{ name: 'projects', type: 'string[]', required: true, constraint: 'The updated, deduped project list in manifest order.' },
|
|
32
|
+
{ name: 'follow_up', type: 'string', required: true, constraint: 'Concrete next commands.' },
|
|
33
|
+
],
|
|
34
|
+
outputKind: 'object',
|
|
35
|
+
effects: ["Appends to the profile manifest's `projects` array. Invalidates the process scope cache."],
|
|
36
|
+
},
|
|
37
|
+
run: async (input) => {
|
|
38
|
+
const { profileId } = loadProfileManifest(input['profile']);
|
|
39
|
+
const { manifest } = addProfileProject(profileId, input['dir']);
|
|
40
|
+
return {
|
|
41
|
+
profile_id: profileId,
|
|
42
|
+
projects: manifest.projects,
|
|
43
|
+
follow_up: `Show the full manifest with \`crtr profile show ${profileId}\`.`,
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
const removeLeaf = defineLeaf({
|
|
48
|
+
name: 'remove',
|
|
49
|
+
description: "remove a project directory from a profile's purview",
|
|
50
|
+
whenToUse: "a profile no longer needs purview over one of its listed project directories — narrow it back. Works even if the directory itself was since deleted.",
|
|
51
|
+
help: {
|
|
52
|
+
name: 'profile project remove',
|
|
53
|
+
summary: "remove a project directory from a profile's manifest",
|
|
54
|
+
params: [
|
|
55
|
+
{
|
|
56
|
+
kind: 'positional',
|
|
57
|
+
name: 'profile',
|
|
58
|
+
required: true,
|
|
59
|
+
constraint: 'Exact profile id, or a unique manifest name.',
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
kind: 'flag',
|
|
63
|
+
name: 'dir',
|
|
64
|
+
type: 'path',
|
|
65
|
+
required: true,
|
|
66
|
+
constraint: 'Directory to remove, matched by its resolved real path against the manifest. Does not require the directory to still exist on disk.',
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
output: [
|
|
70
|
+
{ name: 'profile_id', type: 'string', required: true, constraint: 'Stable directory id.' },
|
|
71
|
+
{ name: 'projects', type: 'string[]', required: true, constraint: 'The updated project list in manifest order.' },
|
|
72
|
+
{ name: 'follow_up', type: 'string', required: true, constraint: 'Concrete next commands.' },
|
|
73
|
+
],
|
|
74
|
+
outputKind: 'object',
|
|
75
|
+
effects: ["Removes an entry from the profile manifest's `projects` array. Invalidates the process scope cache."],
|
|
76
|
+
},
|
|
77
|
+
run: async (input) => {
|
|
78
|
+
const { profileId } = loadProfileManifest(input['profile']);
|
|
79
|
+
const { manifest } = removeProfileProject(profileId, input['dir']);
|
|
80
|
+
return {
|
|
81
|
+
profile_id: profileId,
|
|
82
|
+
projects: manifest.projects,
|
|
83
|
+
follow_up: `Show the full manifest with \`crtr profile show ${profileId}\`.`,
|
|
84
|
+
};
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
export const projectBranch = defineBranch({
|
|
88
|
+
name: 'project',
|
|
89
|
+
description: "widen or narrow a profile's project purview",
|
|
90
|
+
whenToUse: "changing which project directories a profile can see (its memory + config resolution) after creation — add or remove one.",
|
|
91
|
+
help: {
|
|
92
|
+
name: 'profile project',
|
|
93
|
+
summary: "manage a profile's project purview — the directories it resolves memory and config from",
|
|
94
|
+
model: "A profile's `projects` list is the set of directories it inherits memory and config from. `add` widens that purview by one directory (deduped by real path); `remove` narrows it. Read the current list with `crtr profile show`. Purview is independent of a per-cwd default pin (`crtr profile default`) — widening purview never changes which profile a directory defaults to.",
|
|
95
|
+
},
|
|
96
|
+
children: [addLeaf, removeLeaf],
|
|
97
|
+
});
|
|
@@ -44,7 +44,7 @@ export const showLeaf = defineLeaf({
|
|
|
44
44
|
path: profileRoot(profileId),
|
|
45
45
|
config_path: configPath,
|
|
46
46
|
config_exists: existsSync(configPath),
|
|
47
|
-
follow_up: `Add a project with \`crtr profile
|
|
47
|
+
follow_up: `Add a project with \`crtr profile project add ${profileId} --dir <path>\`, or rename with \`crtr profile rename ${profileId} --name <name>\`.`,
|
|
48
48
|
};
|
|
49
49
|
},
|
|
50
50
|
});
|
package/dist/commands/profile.js
CHANGED
|
@@ -1,33 +1,32 @@
|
|
|
1
1
|
// `crtr profile` subtree — the profile store (spec §2.1/§2.5): agent
|
|
2
2
|
// identities defined at the user root, each owning a manifest naming project
|
|
3
|
-
// directories plus its own memory store.
|
|
4
|
-
// verbs fits the 5-7 hard cap.
|
|
3
|
+
// directories plus its own memory store.
|
|
5
4
|
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
5
|
+
// Five flat manifest verbs (new/list/show/rename/delete) plus two sub-nouns:
|
|
6
|
+
// `project` (the profile's project purview) and `default` (the per-cwd default
|
|
7
|
+
// pin). Seven children — at the breadth cap. Every manifest verb but
|
|
8
|
+
// `new`/`list` takes <profile> as an exact id OR a unique manifest name.
|
|
10
9
|
import { defineBranch } from '../core/command.js';
|
|
11
10
|
import { newLeaf } from './profile/new.js';
|
|
12
11
|
import { listLeaf } from './profile/list.js';
|
|
13
12
|
import { showLeaf } from './profile/show.js';
|
|
14
|
-
import { addProjectLeaf } from './profile/add-project.js';
|
|
15
|
-
import { removeProjectLeaf } from './profile/remove-project.js';
|
|
16
13
|
import { renameLeaf } from './profile/rename.js';
|
|
17
14
|
import { deleteLeaf } from './profile/delete.js';
|
|
15
|
+
import { projectBranch } from './profile/project.js';
|
|
16
|
+
import { defaultBranch } from './profile/default.js';
|
|
18
17
|
export function registerProfile() {
|
|
19
18
|
return defineBranch({
|
|
20
19
|
name: 'profile',
|
|
21
20
|
rootEntry: {
|
|
22
21
|
concept: 'an agent identity defined at the user root — a manifest naming project directories plus its own memory store',
|
|
23
22
|
desc: 'create, inspect, and manage profiles',
|
|
24
|
-
useWhen: 'creating a new agent identity, inspecting or listing existing ones,
|
|
23
|
+
useWhen: 'creating a new agent identity, inspecting or listing existing ones, changing a profile\u2019s project purview, or pinning which profile a directory defaults to at startup. Selecting a profile for a one-off run (`--profile`) lives elsewhere.',
|
|
25
24
|
},
|
|
26
25
|
help: {
|
|
27
26
|
name: 'profile',
|
|
28
27
|
summary: 'create, inspect, and manage profiles \u2014 agent identities with their own project purview and memory store',
|
|
29
|
-
model: '`new` creates one from a name (+ at most one initial `--project`). `list` inventories every profile as JSONL. `show` prints one manifest in full. `
|
|
28
|
+
model: '`new` creates one from a name (+ at most one initial `--project`). `list` inventories every profile as JSONL. `show` prints one manifest in full. `rename` changes only the manifest name \u2014 the profile_id is stable across a rename. `delete` removes a profile\u2019s directory (manifest + its own memory store) entirely and does not touch the project directories it pointed at. `project` widens/narrows a profile\u2019s project purview (the dirs it resolves memory + config from). `default` pins which profile a directory defaults to at startup so the chooser stops asking \u2014 directory-scoped, independent of purview. Every manifest verb but `new`/`list` takes <profile> as an exact id or a unique manifest name; an ambiguous name fails listing every matching id.',
|
|
30
29
|
},
|
|
31
|
-
children: [newLeaf, listLeaf, showLeaf,
|
|
30
|
+
children: [newLeaf, listLeaf, showLeaf, renameLeaf, deleteLeaf, projectBranch, defaultBranch],
|
|
32
31
|
});
|
|
33
32
|
}
|