@crouton-kit/crouter 0.3.47 → 0.3.49
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 +0 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/frontmatter-rules/index.ts +5 -3
- package/dist/clients/attach/attach-cmd.js +425 -425
- package/dist/clients/attach/titled-editor.js +6 -3
- package/dist/commands/human/shared.js +7 -3
- 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 +89 -1
- package/dist/commands/sys/sync.js +242 -12
- package/dist/core/__tests__/broker-sdk-wiring.test.js +7 -5
- package/dist/core/__tests__/context-intro.test.js +12 -19
- 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 +15 -7
- package/dist/core/runtime/bearings.js +26 -85
- package/dist/core/runtime/broker.js +5 -4
- 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/on-read.js +9 -6
- 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 +2 -2
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/nested-context.ts +0 -327
- 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),
|
|
@@ -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;
|
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
|
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// Regression: `crtr sys sync` is the one-shot migration path from legacy
|
|
2
2
|
// Agent Skill bundles into crouter memory docs. The hard cut removes SKILL.md as
|
|
3
|
-
// an active guidance surface, but must keep the converter.
|
|
3
|
+
// an active guidance surface, but must keep the converter. It is ALSO the
|
|
4
|
+
// migration path for CLAUDE.md/AGENTS.md/.claude/rules — the hard cut that
|
|
5
|
+
// removes crtr's own bolted-on project-context loader (bearings.ts,
|
|
6
|
+
// nested-context.ts) in favor of the document substrate.
|
|
4
7
|
//
|
|
5
8
|
// Run: node --import tsx/esm --test src/commands/sys/__tests__/sync-import.test.ts
|
|
6
9
|
import { afterEach, beforeEach, test } from 'node:test';
|
|
@@ -10,6 +13,12 @@ import { tmpdir } from 'node:os';
|
|
|
10
13
|
import { join } from 'node:path';
|
|
11
14
|
import { sysSyncLeaf } from '../sync.js';
|
|
12
15
|
import { parseFrontmatterGeneric } from '../../../core/frontmatter.js';
|
|
16
|
+
import { gitSync } from '../../../core/git.js';
|
|
17
|
+
function git(args, cwd) {
|
|
18
|
+
const res = gitSync(args, cwd);
|
|
19
|
+
if (res.status !== 0)
|
|
20
|
+
throw new Error(`git ${args.join(' ')} failed in ${cwd}: ${res.stderr || res.stdout}`);
|
|
21
|
+
}
|
|
13
22
|
let home;
|
|
14
23
|
let prevHome;
|
|
15
24
|
beforeEach(() => {
|
|
@@ -70,3 +79,82 @@ test('permanently ignores selected SKILL.md bundles', async () => {
|
|
|
70
79
|
assert.notEqual(rendered, undefined);
|
|
71
80
|
assert.match(rendered, /would ignore/);
|
|
72
81
|
});
|
|
82
|
+
test('migrates CLAUDE.md and a .claude/rules file into that dir\'s own .crouter/memory/, copy-not-move, skip-existing, --overwrite replaces', async () => {
|
|
83
|
+
const source = mkdtempSync(join(tmpdir(), 'crtr-sys-sync-projectdocs-'));
|
|
84
|
+
writeFileSync(join(source, 'CLAUDE.md'), '# Demo Project\n\nBuild then commit.\n', 'utf8');
|
|
85
|
+
mkdirSync(join(source, '.claude', 'rules'), { recursive: true });
|
|
86
|
+
writeFileSync(join(source, '.claude', 'rules', 'foo.md'), '---\nname: foo\ndescription: Applies when touching src files.\npaths:\n - "src/**/*.ts"\n---\n\nDo the foo thing.\n', 'utf8');
|
|
87
|
+
const projectRows = (r) => r.results.filter((row) => row.target.includes('.crouter/memory'));
|
|
88
|
+
const dry = await sysSyncLeaf.run({ source, dryRun: true });
|
|
89
|
+
const dryRows = projectRows(dry);
|
|
90
|
+
assert.equal(dryRows.length, 2, 'both the AGENTS.md and the rule dry-run as candidates');
|
|
91
|
+
assert.ok(dryRows.every((r) => r.status === 'would-import'));
|
|
92
|
+
assert.equal(existsSync(join(source, '.crouter', 'memory', 'AGENTS.md')), false, 'dry-run writes nothing');
|
|
93
|
+
await sysSyncLeaf.run({ source });
|
|
94
|
+
const agentsTarget = join(source, '.crouter', 'memory', 'AGENTS.md');
|
|
95
|
+
const ruleTarget = join(source, '.crouter', 'memory', 'foo.md');
|
|
96
|
+
assert.equal(existsSync(agentsTarget), true);
|
|
97
|
+
assert.equal(existsSync(ruleTarget), true);
|
|
98
|
+
const agentsDoc = parseFrontmatterGeneric(readFileSync(agentsTarget, 'utf8'));
|
|
99
|
+
assert.equal(agentsDoc.data?.kind, 'knowledge');
|
|
100
|
+
assert.equal(agentsDoc.data?.['system-prompt-visibility'], 'content');
|
|
101
|
+
assert.equal(agentsDoc.data?.['file-read-visibility'], 'content');
|
|
102
|
+
assert.match(agentsDoc.body, /Build then commit\./);
|
|
103
|
+
const ruleDoc = parseFrontmatterGeneric(readFileSync(ruleTarget, 'utf8'));
|
|
104
|
+
assert.equal(ruleDoc.data?.kind, 'knowledge');
|
|
105
|
+
assert.equal(ruleDoc.data?.['system-prompt-visibility'], 'none');
|
|
106
|
+
assert.equal(ruleDoc.data?.['file-read-visibility'], 'content');
|
|
107
|
+
assert.deepEqual(ruleDoc.data?.['applies-to'], ['src/**/*.ts']);
|
|
108
|
+
assert.match(ruleDoc.body, /Do the foo thing\./);
|
|
109
|
+
// Originals untouched — copy, not move.
|
|
110
|
+
assert.equal(existsSync(join(source, 'CLAUDE.md')), true);
|
|
111
|
+
assert.equal(existsSync(join(source, '.claude', 'rules', 'foo.md')), true);
|
|
112
|
+
// Re-run skips existing targets.
|
|
113
|
+
const rerun = await sysSyncLeaf.run({ source });
|
|
114
|
+
const rerunRows = projectRows(rerun);
|
|
115
|
+
assert.ok(rerunRows.every((r) => r.status === 'skipped'));
|
|
116
|
+
// --overwrite replaces.
|
|
117
|
+
writeFileSync(join(source, 'CLAUDE.md'), '# Demo Project v2\n\nBuild, THEN commit.\n', 'utf8');
|
|
118
|
+
const overwritten = await sysSyncLeaf.run({ source, overwrite: true });
|
|
119
|
+
const overwriteRows = projectRows(overwritten);
|
|
120
|
+
assert.ok(overwriteRows.some((r) => r.status === 'imported'));
|
|
121
|
+
const agentsDoc2 = parseFrontmatterGeneric(readFileSync(agentsTarget, 'utf8'));
|
|
122
|
+
assert.match(agentsDoc2.body, /Build, THEN commit\./);
|
|
123
|
+
rmSync(source, { recursive: true, force: true });
|
|
124
|
+
});
|
|
125
|
+
test('routes the walk-root AGENTS.md doc by the real git repo name, not a worktree\'s own basename', async () => {
|
|
126
|
+
// Regression: running the migration from a linked git worktree (e.g. a
|
|
127
|
+
// canvas node's checkout, whose directory is named after a node id) must
|
|
128
|
+
// not bake that node id into the migrated doc's routing text — it should
|
|
129
|
+
// resolve through to the actual repo name shared by every worktree.
|
|
130
|
+
const base = mkdtempSync(join(tmpdir(), 'crtr-sys-sync-worktree-'));
|
|
131
|
+
const mainRepo = join(base, 'demo-repo');
|
|
132
|
+
mkdirSync(mainRepo, { recursive: true });
|
|
133
|
+
git(['init', '-b', 'main'], mainRepo);
|
|
134
|
+
git(['config', 'user.email', 'test@example.com'], mainRepo);
|
|
135
|
+
git(['config', 'user.name', 'Test'], mainRepo);
|
|
136
|
+
writeFileSync(join(mainRepo, 'README.md'), 'hello\n', 'utf8');
|
|
137
|
+
git(['add', '.'], mainRepo);
|
|
138
|
+
git(['commit', '-m', 'init'], mainRepo);
|
|
139
|
+
// A linked worktree whose basename looks nothing like the repo name.
|
|
140
|
+
const linkedWorktree = join(base, 'mr9xyz12-abcd3456');
|
|
141
|
+
git(['worktree', 'add', linkedWorktree, '-b', 'feature'], mainRepo);
|
|
142
|
+
writeFileSync(join(linkedWorktree, 'CLAUDE.md'), '# Fallback Title\n\nRepo guidance.\n', 'utf8');
|
|
143
|
+
const result = await sysSyncLeaf.run({ source: linkedWorktree });
|
|
144
|
+
assert.equal(result?.imported, 1);
|
|
145
|
+
const target = join(linkedWorktree, '.crouter', 'memory', 'AGENTS.md');
|
|
146
|
+
const doc = parseFrontmatterGeneric(readFileSync(target, 'utf8'));
|
|
147
|
+
assert.equal(doc.data?.['when-and-why-to-read'], 'When working in demo-repo, this knowledge should be read because it is the project\'s operating guide.');
|
|
148
|
+
git(['worktree', 'remove', '--force', linkedWorktree], mainRepo);
|
|
149
|
+
rmSync(base, { recursive: true, force: true });
|
|
150
|
+
});
|
|
151
|
+
test('falls back to the doc H1 title when the walk root is not inside a git repo', async () => {
|
|
152
|
+
const source = mkdtempSync(join(tmpdir(), 'crtr-sys-sync-nogit-'));
|
|
153
|
+
writeFileSync(join(source, 'CLAUDE.md'), '# Standalone Docs Project\n\nDo the thing.\n', 'utf8');
|
|
154
|
+
const result = await sysSyncLeaf.run({ source });
|
|
155
|
+
assert.equal(result?.imported, 1);
|
|
156
|
+
const target = join(source, '.crouter', 'memory', 'AGENTS.md');
|
|
157
|
+
const doc = parseFrontmatterGeneric(readFileSync(target, 'utf8'));
|
|
158
|
+
assert.equal(doc.data?.['when-and-why-to-read'], 'When working in Standalone Docs Project, this knowledge should be read because it is the project\'s operating guide.');
|
|
159
|
+
rmSync(source, { recursive: true, force: true });
|
|
160
|
+
});
|