@north-light/crouter 0.3.221 → 0.3.222
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/client.d.ts +21 -1
- package/dist/api/client.js +34 -0
- package/dist/api/dto/chat-inventory.d.ts +13 -0
- package/dist/api/dto/human-requests.d.ts +88 -0
- package/dist/api/dto/human-requests.js +4 -0
- package/dist/api/dto/human.d.ts +3 -0
- package/dist/api/dto/reviews.d.ts +2 -0
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +1 -0
- package/dist/api/routes.d.ts +7 -0
- package/dist/api/routes.js +10 -0
- package/dist/builtin-memory/00-runtime-base/00-authoring.md +31 -0
- package/dist/builtin-memory/00-runtime-base/01-escalation.md +14 -0
- package/dist/builtin-memory/{insights/listen.md → 00-runtime-base/02-insight-capture.md} +1 -0
- package/dist/builtin-memory/02-turn-lifecycle/00-ending-a-turn.md +27 -0
- package/dist/builtin-memory/{02-lifecycle/01-resident.md → 02-turn-lifecycle/02-resident.md} +5 -0
- package/dist/builtin-memory/04-base-worker.md +4 -8
- package/dist/builtin-memory/04-orchestration-kernel.md +1 -1
- package/dist/builtin-memory/05-kinds/advisor/01-orchestrator.md +1 -0
- package/dist/builtin-memory/05-kinds/advisor/advice-contract.md +1 -0
- package/dist/builtin-memory/05-kinds/design/00-base.md +2 -1
- package/dist/builtin-memory/05-kinds/design/01-orchestrator.md +2 -1
- package/dist/builtin-memory/05-kinds/design/design-contract.md +19 -0
- package/dist/builtin-memory/05-kinds/developer/00-base.md +1 -0
- package/dist/builtin-memory/05-kinds/developer/01-orchestrator.md +1 -0
- package/dist/builtin-memory/05-kinds/explore/00-base.md +1 -0
- package/dist/builtin-memory/05-kinds/explore/01-orchestrator.md +1 -0
- package/dist/builtin-memory/05-kinds/general/00-base.md +1 -0
- package/dist/builtin-memory/05-kinds/plan/00-base.md +2 -1
- package/dist/builtin-memory/05-kinds/plan/01-orchestrator.md +2 -1
- package/dist/builtin-memory/05-kinds/plan/plan-contract.md +28 -0
- package/dist/builtin-memory/05-kinds/plan/reviewers/architecture-fit.md +1 -0
- package/dist/builtin-memory/05-kinds/plan/reviewers/code-smells.md +1 -0
- package/dist/builtin-memory/05-kinds/plan/reviewers/lens-contract.md +1 -0
- package/dist/builtin-memory/05-kinds/plan/reviewers/pattern-consistency.md +1 -0
- package/dist/builtin-memory/05-kinds/plan/reviewers/requirements-coverage.md +1 -0
- package/dist/builtin-memory/05-kinds/plan/reviewers/security.md +1 -0
- package/dist/builtin-memory/05-kinds/review/00-base.md +1 -0
- package/dist/builtin-memory/05-kinds/review/01-orchestrator.md +1 -0
- package/dist/builtin-memory/05-kinds/review/companion/00-base.md +1 -0
- package/dist/builtin-memory/05-kinds/review/security-findings.md +1 -0
- package/dist/builtin-memory/05-kinds/spec/00-base.md +4 -3
- package/dist/builtin-memory/05-kinds/spec/01-orchestrator.md +1 -0
- package/dist/builtin-memory/05-kinds/spec/requirements.md +1 -0
- package/dist/builtin-memory/design/guide.md +35 -0
- package/dist/builtin-memory/design/roadmap.md +21 -0
- package/dist/builtin-memory/insights/capture.md +1 -1
- package/dist/builtin-memory/internal/plugins.md +10 -1
- package/dist/builtin-memory/internal/storage-tiers.md +1 -1
- package/dist/builtin-memory/plan/roadmap.md +6 -28
- package/dist/builtin-memory/spec/guide.md +19 -8
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +28 -15
- package/dist/clients/attach/render/markdown-source.js +106 -1
- package/dist/clients/attach/session/file-links.d.ts +13 -4
- package/dist/clients/attach/session/file-links.js +54 -58
- package/dist/clients/attach/viewer.js +525 -523
- package/dist/clients/inbox/controller.js +1 -1
- package/dist/clients/inbox/resolve.d.ts +1 -0
- package/dist/clients/inbox/review/review-client.js +3 -1
- package/dist/commands/__tests__/human.test.js +2 -2
- package/dist/commands/human/request.d.ts +2 -0
- package/dist/commands/human/request.js +281 -0
- package/dist/commands/human.js +5 -2
- package/dist/commands/sys/config.js +2 -2
- package/dist/commands/sys/doctor.js +54 -2
- package/dist/core/__tests__/broker-extension-canvas-db-boundary.test.js +7 -4
- package/dist/core/__tests__/fixtures/memory-slash-live-probe.d.ts +1 -0
- package/dist/core/__tests__/fixtures/memory-slash-live-probe.js +71 -0
- package/dist/core/__tests__/human-action-delivery.test.d.ts +1 -0
- package/dist/core/__tests__/human-action-delivery.test.js +140 -0
- package/dist/core/__tests__/human-actions.test.d.ts +1 -0
- package/dist/core/__tests__/human-actions.test.js +116 -0
- package/dist/core/__tests__/inline-memory-refs.test.js +1 -1
- package/dist/core/__tests__/profile-project-memory-delivery.test.js +1 -1
- package/dist/core/__tests__/prospective-inventory-capability-parity.test.d.ts +1 -0
- package/dist/core/__tests__/prospective-inventory-capability-parity.test.js +91 -0
- package/dist/core/__tests__/seam/memory-slash-node-relative-inventory.test.d.ts +1 -0
- package/dist/core/__tests__/seam/memory-slash-node-relative-inventory.test.js +127 -0
- package/dist/core/__tests__/seam/prospective-inventory-stdout.test.d.ts +1 -0
- package/dist/core/__tests__/seam/prospective-inventory-stdout.test.js +31 -0
- package/dist/core/canvas/db.js +23 -0
- package/dist/core/canvas/human-deliveries.d.ts +53 -0
- package/dist/core/canvas/human-deliveries.js +75 -0
- package/dist/core/config.d.ts +13 -1
- package/dist/core/config.js +51 -1
- package/dist/core/feed/inbox.d.ts +6 -0
- package/dist/core/feed/inbox.js +9 -1
- package/dist/core/human/action-binding.d.ts +21 -0
- package/dist/core/human/action-binding.js +40 -0
- package/dist/core/human/completion.d.ts +38 -0
- package/dist/core/human/completion.js +27 -0
- package/dist/core/human/convention.d.ts +2 -0
- package/dist/core/human/convention.js +2 -0
- package/dist/core/human/tickets.d.ts +25 -6
- package/dist/core/human/tickets.js +19 -13
- package/dist/core/human/types.d.ts +5 -0
- package/dist/core/human-actions.d.ts +25 -0
- package/dist/core/human-actions.js +101 -0
- package/dist/core/memory-resolver.js +1 -1
- package/dist/core/profiles/select.d.ts +2 -0
- package/dist/core/profiles/select.js +21 -4
- package/dist/core/runtime/broker/frame-dispatch.js +2 -5
- package/dist/core/runtime/broker-inventory.d.ts +1 -2
- package/dist/core/runtime/broker-inventory.js +2 -77
- package/dist/core/runtime/broker-persona-guidance.js +1 -1
- package/dist/core/runtime/broker.js +4 -4
- package/dist/core/runtime/chat-inventory-rows.d.ts +8 -0
- package/dist/core/runtime/chat-inventory-rows.js +105 -0
- package/dist/core/runtime/command-surface.d.ts +8 -3
- package/dist/core/runtime/command-surface.js +42 -6
- package/dist/core/runtime/launch-target.d.ts +25 -0
- package/dist/core/runtime/launch-target.js +54 -0
- package/dist/core/runtime/persona.js +3 -3
- package/dist/core/runtime/prospective-inventory-cli.d.ts +1 -0
- package/dist/core/runtime/prospective-inventory-cli.js +61 -0
- package/dist/core/runtime/prospective-inventory.d.ts +10 -0
- package/dist/core/runtime/prospective-inventory.js +88 -0
- package/dist/core/runtime/spawn.d.ts +3 -1
- package/dist/core/runtime/spawn.js +5 -3
- package/dist/core/substrate/on-read.js +17 -28
- package/dist/core/substrate/render-node.d.ts +3 -2
- package/dist/core/substrate/render-node.js +3 -2
- package/dist/core/substrate/render.js +51 -19
- package/dist/core/substrate/schema.d.ts +5 -1
- package/dist/core/substrate/schema.js +4 -4
- package/dist/core/user-settings.d.ts +4 -0
- package/dist/core/user-settings.js +1 -0
- package/dist/daemon/api/__tests__/profile-launch-gates.test.js +52 -3
- package/dist/daemon/api/handlers/human-requests.d.ts +2 -0
- package/dist/daemon/api/handlers/human-requests.js +409 -0
- package/dist/daemon/api/handlers/human.js +3 -0
- package/dist/daemon/api/handlers/inbox.js +3 -0
- package/dist/daemon/api/handlers/nodes.d.ts +1 -3
- package/dist/daemon/api/handlers/nodes.js +11 -46
- package/dist/daemon/api/handlers/prospective-chat-inventory.d.ts +2 -0
- package/dist/daemon/api/handlers/prospective-chat-inventory.js +59 -0
- package/dist/daemon/api/handlers/reviews.js +10 -2
- package/dist/daemon/api/server.js +4 -0
- package/dist/daemon/crtrd.js +6 -0
- package/dist/daemon/human/deliver-action.d.ts +16 -0
- package/dist/daemon/human/deliver-action.js +168 -0
- package/dist/daemon/human/finish.d.ts +8 -5
- package/dist/daemon/human/finish.js +45 -6
- package/dist/daemon/human/sweep.js +4 -1
- package/dist/daemon/reconcilers/human-delivery-lane.d.ts +10 -0
- package/dist/daemon/reconcilers/human-delivery-lane.js +41 -0
- package/dist/daemon/review/finish.d.ts +8 -3
- package/dist/daemon/review/finish.js +19 -1
- package/dist/types.d.ts +8 -0
- package/dist/types.js +1 -0
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
- package/dist/builtin-memory/00-runtime-base.md +0 -55
- package/dist/builtin-memory/design.md +0 -55
- /package/dist/builtin-memory/{02-lifecycle/00-terminal.md → 02-turn-lifecycle/01-terminal.md} +0 -0
|
@@ -28,6 +28,7 @@ import { fileRoutes } from './handlers/files.js';
|
|
|
28
28
|
import { focusRoutes } from './handlers/focus.js';
|
|
29
29
|
import { healthRoutes } from './handlers/health.js';
|
|
30
30
|
import { humanRoutes } from './handlers/human.js';
|
|
31
|
+
import { humanRequestRoutes } from './handlers/human-requests.js';
|
|
31
32
|
import { inboxRoutes } from './handlers/inbox.js';
|
|
32
33
|
import { feedbackCommentRoutes } from './handlers/feedback-comments.js';
|
|
33
34
|
import { memoryRoutes } from './handlers/memory.js';
|
|
@@ -35,6 +36,7 @@ import { messageRoutes } from './handlers/messages.js';
|
|
|
35
36
|
import { modelAuthRoutes } from './handlers/modelauth.js';
|
|
36
37
|
import { nodeRoutes } from './handlers/nodes.js';
|
|
37
38
|
import { profileRoutes } from './handlers/profiles.js';
|
|
39
|
+
import { prospectiveChatInventoryRoutes } from './handlers/prospective-chat-inventory.js';
|
|
38
40
|
import { reportRoutes } from './handlers/reports.js';
|
|
39
41
|
import { reviewCommentRoutes } from './handlers/review-comments.js';
|
|
40
42
|
import { reviewRoutes } from './handlers/reviews.js';
|
|
@@ -67,7 +69,9 @@ function buildRouter() {
|
|
|
67
69
|
.registerAll(reviewRoutes)
|
|
68
70
|
.registerAll(reviewCommentRoutes)
|
|
69
71
|
.registerAll(inboxRoutes)
|
|
72
|
+
.registerAll(humanRequestRoutes)
|
|
70
73
|
.registerAll(chatInventoryRoutes)
|
|
74
|
+
.registerAll(prospectiveChatInventoryRoutes)
|
|
71
75
|
.registerAll(feedbackCommentRoutes);
|
|
72
76
|
}
|
|
73
77
|
/** Is this request an attach upgrade — `GET /v1/nodes/{id}/attach` — and if so,
|
package/dist/daemon/crtrd.js
CHANGED
|
@@ -50,6 +50,7 @@ import { crtrHome, isSafeNodeId } from '../core/canvas/paths.js';
|
|
|
50
50
|
import { listNodes, migrateLegacyPidIdentities, } from '../core/canvas/index.js';
|
|
51
51
|
import { createApiServer } from './api/server.js';
|
|
52
52
|
import { reconcileUndeliveredTickets } from './human/sweep.js';
|
|
53
|
+
import { recoverRunningActionDeliveries } from '../core/canvas/human-deliveries.js';
|
|
53
54
|
import { sweepReviews } from './review/sweep.js';
|
|
54
55
|
import { pidfilePath } from './pidfile.js';
|
|
55
56
|
import { rendererWarning } from '../core/runtime/package-health.js';
|
|
@@ -57,6 +58,7 @@ import { DEFAULT_INTERVAL_MS } from './supervise-cadence.js';
|
|
|
57
58
|
import { BrokerSupervisionReconciler } from './reconcilers/broker-supervision.js';
|
|
58
59
|
import { ControllerDeathReconciler, DormantInboxReconciler } from './reconcilers/dormant-inbox.js';
|
|
59
60
|
import { CronLaneReconciler } from './reconcilers/cron-lane.js';
|
|
61
|
+
import { HumanDeliveryLaneReconciler } from './reconcilers/human-delivery-lane.js';
|
|
60
62
|
import { PendingReviewSubmitReconciler } from './reconcilers/pending-review-submit.js';
|
|
61
63
|
import { StorageMaintenanceReconciler } from './reconcilers/storage-maintenance.js';
|
|
62
64
|
import { captureLivenessSnapshot, capturePidCommand, captureTeardownSnapshot, isPidAlive, killProcessTreePids, } from '../core/canvas/pid.js';
|
|
@@ -312,6 +314,7 @@ const brokerSupervision = new BrokerSupervisionReconciler();
|
|
|
312
314
|
const controllerDeath = new ControllerDeathReconciler();
|
|
313
315
|
const dormantInbox = new DormantInboxReconciler();
|
|
314
316
|
const cronLane = new CronLaneReconciler();
|
|
317
|
+
const humanDeliveryLane = new HumanDeliveryLaneReconciler();
|
|
315
318
|
const pendingReviewSubmit = new PendingReviewSubmitReconciler();
|
|
316
319
|
const storageMaintenance = new StorageMaintenanceReconciler();
|
|
317
320
|
/** Supervision tick over the daemon's current live set. */
|
|
@@ -336,6 +339,7 @@ export async function superviseTick(now = Date.now(), lifecycle = directTickLife
|
|
|
336
339
|
controllerDeath.run(now, { rows });
|
|
337
340
|
dormantInbox.run(now, { rows, fleet, hasBrokerCapacity });
|
|
338
341
|
cronLane.run(now, { lifecycle, hasBrokerCapacity });
|
|
342
|
+
humanDeliveryLane.run(now, { lifecycle });
|
|
339
343
|
pendingReviewSubmit.run({ lifecycle });
|
|
340
344
|
storageMaintenance.run(now);
|
|
341
345
|
}
|
|
@@ -652,6 +656,8 @@ export async function runDaemon(opts = {}) {
|
|
|
652
656
|
await operationIdContext.fresh(async () => {
|
|
653
657
|
await sweepReviews();
|
|
654
658
|
await reconcileUndeliveredTickets();
|
|
659
|
+
// Unlike a cron one-shot, a completion is at-least-once: destinations deduplicate request IDs, so a dead daemon's running delivery is re-queued immediately.
|
|
660
|
+
recoverRunningActionDeliveries(Date.now());
|
|
655
661
|
});
|
|
656
662
|
// Boot reconciliation clears stale process identities before any tick can
|
|
657
663
|
// inspect them, then migrates legacy identities only with proven same-boot
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type ActionDeliveryRecord, type ActionDeliverySettlement } from '../../core/canvas/human-deliveries.js';
|
|
2
|
+
export declare const ACTION_DELIVERY_TIMEOUT_MS = 120000;
|
|
3
|
+
export declare const ACTION_DELIVERY_STDERR_TAIL_BYTES: number;
|
|
4
|
+
/** Durable exponential retry delay for the attempt that just failed. */
|
|
5
|
+
export declare function actionDeliveryBackoffMs(attempt: number): number;
|
|
6
|
+
export interface ActionDeliveryProcessOutcome {
|
|
7
|
+
exitCode: number | null;
|
|
8
|
+
signal: string | null;
|
|
9
|
+
timedOut: boolean;
|
|
10
|
+
spawnError?: Error;
|
|
11
|
+
stderr: string;
|
|
12
|
+
}
|
|
13
|
+
/** Process contract mapping. stdout deliberately does not participate. */
|
|
14
|
+
export declare function actionDeliverySettlementForOutcome(outcome: ActionDeliveryProcessOutcome, attempt: number, now: number): ActionDeliverySettlement;
|
|
15
|
+
/** Spawn and settle one already-claimed action delivery. */
|
|
16
|
+
export declare function deliverAction(delivery: ActionDeliveryRecord, claimOwner: string): Promise<void>;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { settleActionDelivery, } from '../../core/canvas/human-deliveries.js';
|
|
3
|
+
import { killProcessGroup } from '../../core/canvas/pid.js';
|
|
4
|
+
import { buildOperationalEnvBase } from '../../core/runtime/spawn-env.js';
|
|
5
|
+
export const ACTION_DELIVERY_TIMEOUT_MS = 120_000;
|
|
6
|
+
const ACTION_DELIVERY_KILL_GRACE_MS = 2_000;
|
|
7
|
+
export const ACTION_DELIVERY_STDERR_TAIL_BYTES = 4 * 1024;
|
|
8
|
+
/** Durable exponential retry delay for the attempt that just failed. */
|
|
9
|
+
export function actionDeliveryBackoffMs(attempt) {
|
|
10
|
+
return Math.min(5_000 * 2 ** (attempt - 1), 3_600_000);
|
|
11
|
+
}
|
|
12
|
+
/** Process contract mapping. stdout deliberately does not participate. */
|
|
13
|
+
export function actionDeliverySettlementForOutcome(outcome, attempt, now) {
|
|
14
|
+
if (outcome.spawnError !== undefined) {
|
|
15
|
+
return {
|
|
16
|
+
state: 'pending',
|
|
17
|
+
nextAttemptAt: now + actionDeliveryBackoffMs(attempt),
|
|
18
|
+
failure: failure('spawn_error', { message: outcome.spawnError.message, stderr: outcome.stderr }),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
if (outcome.timedOut) {
|
|
22
|
+
return {
|
|
23
|
+
state: 'pending',
|
|
24
|
+
nextAttemptAt: now + actionDeliveryBackoffMs(attempt),
|
|
25
|
+
// The kill escalation's own signal distinguishes the graceful term from the SIGKILL.
|
|
26
|
+
failure: failure('timeout', { ...(outcome.signal === null ? {} : { signal: outcome.signal }), stderr: outcome.stderr }),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
if (outcome.signal !== null) {
|
|
30
|
+
return {
|
|
31
|
+
state: 'pending',
|
|
32
|
+
nextAttemptAt: now + actionDeliveryBackoffMs(attempt),
|
|
33
|
+
failure: failure('signal', { signal: outcome.signal, stderr: outcome.stderr }),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
if (outcome.exitCode === 0)
|
|
37
|
+
return { state: 'accepted' };
|
|
38
|
+
if (outcome.exitCode === 78) {
|
|
39
|
+
return { state: 'permanent_failed', failure: failure('exit', { exitCode: 78, stderr: outcome.stderr }) };
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
state: 'pending',
|
|
43
|
+
nextAttemptAt: now + actionDeliveryBackoffMs(attempt),
|
|
44
|
+
failure: failure('exit', { exitCode: outcome.exitCode ?? undefined, stderr: outcome.stderr }),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function failure(kind, fields) {
|
|
48
|
+
return { kind, ...fields };
|
|
49
|
+
}
|
|
50
|
+
function stderrTailCapture() {
|
|
51
|
+
let tail = Buffer.alloc(0);
|
|
52
|
+
return {
|
|
53
|
+
take(chunk) {
|
|
54
|
+
tail = chunk.length >= ACTION_DELIVERY_STDERR_TAIL_BYTES
|
|
55
|
+
? chunk.subarray(chunk.length - ACTION_DELIVERY_STDERR_TAIL_BYTES)
|
|
56
|
+
: Buffer.concat([tail, chunk]).subarray(Math.max(0, tail.length + chunk.length - ACTION_DELIVERY_STDERR_TAIL_BYTES));
|
|
57
|
+
},
|
|
58
|
+
decode() {
|
|
59
|
+
return tail.toString('utf8');
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function completionForAttempt(completion, attempt) {
|
|
64
|
+
const document = completion;
|
|
65
|
+
const delivery = document.delivery;
|
|
66
|
+
return JSON.stringify({ ...document, delivery: { ...delivery, attempt } });
|
|
67
|
+
}
|
|
68
|
+
/** Spawn and settle one already-claimed action delivery. */
|
|
69
|
+
export async function deliverAction(delivery, claimOwner) {
|
|
70
|
+
const stderr = stderrTailCapture();
|
|
71
|
+
let completion;
|
|
72
|
+
try {
|
|
73
|
+
completion = completionForAttempt(delivery.completion, delivery.attempt);
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
settle(delivery, claimOwner, actionDeliverySettlementForOutcome({
|
|
77
|
+
exitCode: null,
|
|
78
|
+
signal: null,
|
|
79
|
+
timedOut: false,
|
|
80
|
+
spawnError: error,
|
|
81
|
+
stderr: stderr.decode(),
|
|
82
|
+
}, delivery.attempt, Date.now()));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
let child;
|
|
86
|
+
try {
|
|
87
|
+
child = spawn(delivery.argv[0], delivery.argv.slice(1), {
|
|
88
|
+
cwd: delivery.cwd,
|
|
89
|
+
env: actionDeliveryEnv(delivery.cwd),
|
|
90
|
+
detached: true,
|
|
91
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
settle(delivery, claimOwner, actionDeliverySettlementForOutcome({
|
|
96
|
+
exitCode: null,
|
|
97
|
+
signal: null,
|
|
98
|
+
timedOut: false,
|
|
99
|
+
spawnError: error,
|
|
100
|
+
stderr: stderr.decode(),
|
|
101
|
+
}, delivery.attempt, Date.now()));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
await new Promise((resolve) => {
|
|
105
|
+
let settled = false;
|
|
106
|
+
let timedOut = false;
|
|
107
|
+
let killTimer;
|
|
108
|
+
let timeout;
|
|
109
|
+
const finish = (outcome) => {
|
|
110
|
+
if (settled)
|
|
111
|
+
return;
|
|
112
|
+
settled = true;
|
|
113
|
+
if (timeout !== undefined)
|
|
114
|
+
clearTimeout(timeout);
|
|
115
|
+
if (killTimer !== undefined)
|
|
116
|
+
clearTimeout(killTimer);
|
|
117
|
+
child.stdin?.destroy();
|
|
118
|
+
child.stdout?.destroy();
|
|
119
|
+
child.stderr?.destroy();
|
|
120
|
+
settle(delivery, claimOwner, actionDeliverySettlementForOutcome(outcome, delivery.attempt, Date.now()));
|
|
121
|
+
resolve();
|
|
122
|
+
};
|
|
123
|
+
child.stdout?.on('data', () => { }); // Drain diagnostics; stdout is never a control channel.
|
|
124
|
+
child.stderr?.on('data', (chunk) => stderr.take(chunk));
|
|
125
|
+
child.stdin?.on('error', () => { }); // A child may exit before draining a large completion; its exit code remains authoritative.
|
|
126
|
+
child.on('error', (error) => {
|
|
127
|
+
finish({ exitCode: null, signal: null, timedOut: false, spawnError: error, stderr: stderr.decode() });
|
|
128
|
+
});
|
|
129
|
+
child.on('exit', (exitCode, signal) => {
|
|
130
|
+
finish({
|
|
131
|
+
exitCode,
|
|
132
|
+
signal,
|
|
133
|
+
timedOut,
|
|
134
|
+
stderr: stderr.decode(),
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
const killWithEscalation = () => {
|
|
138
|
+
if (child.pid === undefined)
|
|
139
|
+
return;
|
|
140
|
+
killProcessGroup(child.pid);
|
|
141
|
+
killTimer = setTimeout(() => killProcessGroup(child.pid, 'SIGKILL'), ACTION_DELIVERY_KILL_GRACE_MS);
|
|
142
|
+
killTimer.unref?.();
|
|
143
|
+
};
|
|
144
|
+
// The deadline starts at spawn, not after stdin write or any output.
|
|
145
|
+
timeout = setTimeout(() => {
|
|
146
|
+
timedOut = true;
|
|
147
|
+
killWithEscalation();
|
|
148
|
+
}, ACTION_DELIVERY_TIMEOUT_MS);
|
|
149
|
+
timeout.unref?.();
|
|
150
|
+
child.stdin?.end(completion, 'utf8');
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
/** The delivery child crosses the same consent boundary as every other
|
|
154
|
+
* crouter spawn: nothing of crtrd's own environment reaches it except what
|
|
155
|
+
* `spawnEnv.allow` and the operational base admit for the action's own cwd.
|
|
156
|
+
* No profile participates — an action is declared in scope config alone. */
|
|
157
|
+
function actionDeliveryEnv(cwd) {
|
|
158
|
+
const env = buildOperationalEnvBase({ targetCwd: cwd, targetProfileId: null });
|
|
159
|
+
// Runtime identity, not host state: an action that calls `crtr` must reach
|
|
160
|
+
// the daemon that delivered to it, exactly as a cron body does.
|
|
161
|
+
const canvasHome = process.env['CRTR_HOME'];
|
|
162
|
+
if (canvasHome !== undefined && canvasHome !== '')
|
|
163
|
+
env['CRTR_HOME'] = canvasHome;
|
|
164
|
+
return env;
|
|
165
|
+
}
|
|
166
|
+
function settle(delivery, claimOwner, settlement) {
|
|
167
|
+
settleActionDelivery(delivery.requestId, delivery.attempt, claimOwner, settlement, Date.now());
|
|
168
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type CancelTicketOptions, type CompletePageOptions } from '../../core/human/tickets.js';
|
|
1
2
|
import type { TicketResult } from '../../core/human/types.js';
|
|
2
3
|
/** Every ticket-ending action retires the ticket's feedback companion — done
|
|
3
4
|
* with a canonical final when approved, never left resident. Idempotent and replay-safe; a
|
|
@@ -11,12 +12,14 @@ export declare function settleFeedbackCompanion(ticketId: string, outcome: 'appr
|
|
|
11
12
|
* through this function, so the reply-route descriptor is the sole authority
|
|
12
13
|
* on whether completion reaches a bridge.
|
|
13
14
|
*/
|
|
14
|
-
export declare function resolvePageTicket(ticketId: string, responses: unknown): Promise<TicketResult>;
|
|
15
|
+
export declare function resolvePageTicket(ticketId: string, responses: unknown, opts?: CompletePageOptions): Promise<TicketResult>;
|
|
15
16
|
/** Cancel a ticket with the store's first-writer-wins publication. */
|
|
16
|
-
export declare function cancelHumanTicket(ticketId: string, opts:
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
export declare function cancelHumanTicket(ticketId: string, opts: CancelTicketOptions): Promise<TicketResult>;
|
|
18
|
+
/** Schedule the settled request's completion action, if it bound one. Only the
|
|
19
|
+
* settlement winner calls this; the daemon-start sweep replays it for a
|
|
20
|
+
* settlement that died before enqueueing, and the insert is idempotent on the
|
|
21
|
+
* request id so neither path can create a second delivery. */
|
|
22
|
+
export declare function releaseActionDelivery(ticketId: string): void;
|
|
20
23
|
/**
|
|
21
24
|
* Deliver a terminal ticket result to its bridge. The canonical response is the
|
|
22
25
|
* receipt: every action below is replay-safe, so lost delivery can be retried
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { ApiError } from '../../api/index.js';
|
|
2
2
|
import { getRow, subscribersOf } from '../../core/canvas/canvas.js';
|
|
3
|
-
import { ticketDir } from '../../core/human/root.js';
|
|
3
|
+
import { opaqueInboxTicketId, ticketDir } from '../../core/human/root.js';
|
|
4
|
+
import { readTicketActionBinding } from '../../core/human/action-binding.js';
|
|
5
|
+
import { completionEventFor, composeHumanCompletion } from '../../core/human/completion.js';
|
|
6
|
+
import { enqueueActionDelivery } from '../../core/canvas/human-deliveries.js';
|
|
4
7
|
import { parsePage } from '../../core/human/page.js';
|
|
5
8
|
import { cancelTicket, projectReviewOutput, readTicketResult, takeoverAndCompletePage, } from '../../core/human/tickets.js';
|
|
6
9
|
import { describePageAnswer } from '../../core/human/answer.js';
|
|
@@ -151,10 +154,11 @@ async function deliverOrConflict(ticketId) {
|
|
|
151
154
|
* through this function, so the reply-route descriptor is the sole authority
|
|
152
155
|
* on whether completion reaches a bridge.
|
|
153
156
|
*/
|
|
154
|
-
export async function resolvePageTicket(ticketId, responses) {
|
|
155
|
-
const completed = takeoverAndCompletePage(ticketDir(ticketId), responses, { host: 'crtrd', pid: process.pid });
|
|
157
|
+
export async function resolvePageTicket(ticketId, responses, opts = {}) {
|
|
158
|
+
const completed = takeoverAndCompletePage(ticketDir(ticketId), responses, { host: 'crtrd', pid: process.pid }, opts);
|
|
156
159
|
if (!completed.won)
|
|
157
160
|
return deliverOrConflict(ticketId);
|
|
161
|
+
releaseActionDelivery(ticketId);
|
|
158
162
|
try {
|
|
159
163
|
await deliverTerminalResult(ticketId);
|
|
160
164
|
}
|
|
@@ -168,6 +172,7 @@ export async function cancelHumanTicket(ticketId, opts) {
|
|
|
168
172
|
const canceled = cancelTicket(ticketDir(ticketId), opts);
|
|
169
173
|
if (canceled.status === 'already_resolved')
|
|
170
174
|
return deliverOrConflict(ticketId);
|
|
175
|
+
releaseActionDelivery(ticketId);
|
|
171
176
|
try {
|
|
172
177
|
await deliverTerminalResult(ticketId);
|
|
173
178
|
}
|
|
@@ -176,6 +181,40 @@ export async function cancelHumanTicket(ticketId, opts) {
|
|
|
176
181
|
}
|
|
177
182
|
return canceled.result;
|
|
178
183
|
}
|
|
184
|
+
/** Schedule the settled request's completion action, if it bound one. Only the
|
|
185
|
+
* settlement winner calls this; the daemon-start sweep replays it for a
|
|
186
|
+
* settlement that died before enqueueing, and the insert is idempotent on the
|
|
187
|
+
* request id so neither path can create a second delivery. */
|
|
188
|
+
export function releaseActionDelivery(ticketId) {
|
|
189
|
+
try {
|
|
190
|
+
const dir = ticketDir(ticketId);
|
|
191
|
+
const binding = readTicketActionBinding(dir);
|
|
192
|
+
if (binding === null)
|
|
193
|
+
return;
|
|
194
|
+
const result = readTicketResult(dir);
|
|
195
|
+
if (result === null || result.kind === 'review')
|
|
196
|
+
return;
|
|
197
|
+
const requestId = opaqueInboxTicketId(ticketId);
|
|
198
|
+
enqueueActionDelivery({
|
|
199
|
+
requestId,
|
|
200
|
+
argv: binding.argv,
|
|
201
|
+
cwd: binding.cwd,
|
|
202
|
+
completion: composeHumanCompletion({ requestId, result, binding, source: pageSource(dir) }),
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
// The canonical outcome is already durable; the sweep replays this.
|
|
207
|
+
emitEvent({ level: 'error', event: 'human.action_delivery.enqueue_failed', node_id: ticketId, error });
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
function pageSource(dir) {
|
|
211
|
+
try {
|
|
212
|
+
return parsePage(dir).source ?? {};
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
return {};
|
|
216
|
+
}
|
|
217
|
+
}
|
|
179
218
|
/**
|
|
180
219
|
* Deliver a terminal ticket result to its bridge. The canonical response is the
|
|
181
220
|
* receipt: every action below is replay-safe, so lost delivery can be retried
|
|
@@ -207,9 +246,9 @@ export async function deliverTerminalResult(ticketId) {
|
|
|
207
246
|
if (result.kind === 'canceled') {
|
|
208
247
|
const title = pageTitle(dir);
|
|
209
248
|
const reason = result.reason !== undefined && result.reason !== '' ? result.reason : 'no reason was given';
|
|
210
|
-
// A
|
|
211
|
-
//
|
|
212
|
-
const dismissed = result
|
|
249
|
+
// A dismissal is a rejection, not a routing event: say so plainly so the
|
|
250
|
+
// agent acts on the "no" instead of re-raising the same ask.
|
|
251
|
+
const dismissed = completionEventFor(result) === 'dismissed';
|
|
213
252
|
const subject = dismissed
|
|
214
253
|
? (title === undefined ? `human interaction ${ticketId} dismissed without answering` : `human interaction ${ticketId} — “${title}” dismissed without answering`)
|
|
215
254
|
: (title === undefined ? `human interaction ${ticketId} canceled — no answer is coming` : `human interaction ${ticketId} — “${title}” canceled, no answer is coming`);
|
|
@@ -5,7 +5,7 @@ import { getReviewByBridge } from '../../core/review/store.js';
|
|
|
5
5
|
import { emitEvent } from '../../core/events/emit.js';
|
|
6
6
|
import { ticketsRoot } from '../../core/human/root.js';
|
|
7
7
|
import { readTicketResult } from '../../core/human/tickets.js';
|
|
8
|
-
import { deliverTerminalResult, settleFeedbackCompanion } from './finish.js';
|
|
8
|
+
import { deliverTerminalResult, releaseActionDelivery, settleFeedbackCompanion } from './finish.js';
|
|
9
9
|
function isTerminalBridge(status) {
|
|
10
10
|
return status === 'done' || status === 'dead' || status === 'canceled';
|
|
11
11
|
}
|
|
@@ -32,6 +32,9 @@ export async function reconcileUndeliveredTickets() {
|
|
|
32
32
|
// reply bridge exists: inbox-only pages have no bridge row, yet the
|
|
33
33
|
// daemon can die between result publication and companion retirement.
|
|
34
34
|
await settleFeedbackCompanion(entry, ticketResult.kind === 'canceled' ? 'canceled' : 'approved');
|
|
35
|
+
// A settlement that died before enqueueing its completion action still
|
|
36
|
+
// owes that delivery; the insert is idempotent on the request id.
|
|
37
|
+
releaseActionDelivery(entry);
|
|
35
38
|
// The ROW is what carries status; `getNode` would answer from a pruned
|
|
36
39
|
// bridge's surviving meta.json with no status at all.
|
|
37
40
|
const bridge = getRow(entry);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { DetachedWorkLifecycle } from './broker-supervision.js';
|
|
2
|
+
/** One daemon-life identity for guarded action-delivery claims. */
|
|
3
|
+
export declare const humanDeliveryDaemonInstanceId: `${string}-${string}-${string}-${string}-${string}`;
|
|
4
|
+
export interface HumanDeliveryLaneContext {
|
|
5
|
+
lifecycle: DetachedWorkLifecycle;
|
|
6
|
+
}
|
|
7
|
+
/** Admit due action deliveries without making the daemon's serial tick await subprocesses. */
|
|
8
|
+
export declare class HumanDeliveryLaneReconciler {
|
|
9
|
+
run(now: number, ctx: HumanDeliveryLaneContext): void;
|
|
10
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { claimActionDelivery, dueActionDeliveries } from '../../core/canvas/human-deliveries.js';
|
|
3
|
+
import { emitEvent } from '../../core/events/emit.js';
|
|
4
|
+
import { operationIdContext } from '../../core/events/operation-id.js';
|
|
5
|
+
import { deliverAction } from '../human/deliver-action.js';
|
|
6
|
+
/** One daemon-life identity for guarded action-delivery claims. */
|
|
7
|
+
export const humanDeliveryDaemonInstanceId = randomUUID();
|
|
8
|
+
/** Admit due action deliveries without making the daemon's serial tick await subprocesses. */
|
|
9
|
+
export class HumanDeliveryLaneReconciler {
|
|
10
|
+
run(now, ctx) {
|
|
11
|
+
if (!ctx.lifecycle.acceptsDetachedWork())
|
|
12
|
+
return;
|
|
13
|
+
let due;
|
|
14
|
+
try {
|
|
15
|
+
due = dueActionDeliveries(now);
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
operationIdContext.fresh(() => {
|
|
19
|
+
emitEvent({ level: 'error', event: 'human.action_delivery.scan_failed', error });
|
|
20
|
+
});
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
for (const candidate of due) {
|
|
24
|
+
try {
|
|
25
|
+
const claimed = claimActionDelivery(candidate.requestId, now, humanDeliveryDaemonInstanceId);
|
|
26
|
+
if (claimed === null)
|
|
27
|
+
continue;
|
|
28
|
+
ctx.lifecycle.registerDetached(deliverAction(claimed, humanDeliveryDaemonInstanceId).catch((error) => {
|
|
29
|
+
operationIdContext.fresh(() => {
|
|
30
|
+
emitEvent({ level: 'error', event: 'human.action_delivery.failed', error, fields: { request_id: claimed.requestId } });
|
|
31
|
+
});
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
operationIdContext.fresh(() => {
|
|
36
|
+
emitEvent({ level: 'error', event: 'human.action_delivery.claim_failed', error, fields: { request_id: candidate.requestId } });
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -40,8 +40,13 @@ export declare function completeRequestedSubmit(reviewId: string, opts: {
|
|
|
40
40
|
/** The companion's turn ended: complete any submit the human left waiting on
|
|
41
41
|
* it. Never throws — a settlement directive must not depend on delivery. */
|
|
42
42
|
export declare function completeCompanionRequestedSubmit(companionNodeId: string): Promise<void>;
|
|
43
|
-
/**
|
|
44
|
-
|
|
43
|
+
/** `disposition` rides through to the ticket result: the recipient closing a
|
|
44
|
+
* review in their inbox is a dismissal, while the requester withdrawing it
|
|
45
|
+
* stays a cancellation. */
|
|
46
|
+
export interface ReviewCancellationArgs {
|
|
45
47
|
reason?: string;
|
|
46
48
|
actor?: string;
|
|
47
|
-
|
|
49
|
+
disposition?: 'canceled' | 'dismissed';
|
|
50
|
+
}
|
|
51
|
+
/** Cancel an open review once; ticket cancellation remains owned by the retained human-ticket finisher. */
|
|
52
|
+
export declare function cancelReview(reviewId: string, args: ReviewCancellationArgs): Promise<ReviewCancellationFinishOutcome>;
|
|
@@ -9,6 +9,7 @@ import { readReviewSource } from '../../core/review/document.js';
|
|
|
9
9
|
import { signalReviewActivity } from '../../core/review/signal.js';
|
|
10
10
|
import { approveReviewLocked, cancelReviewRow, getReview, getReviewByCompanion, markDelivered, markDeliveryError, markProjected, markProjectionError, requestReviewSubmit, requireVisibleReview, } from '../../core/review/store.js';
|
|
11
11
|
import { ReviewOperationError } from '../../core/review/types.js';
|
|
12
|
+
import { cancelPendingEntries } from '../../core/feed/inbox.js';
|
|
12
13
|
import { emitEvent } from '../../core/events/emit.js';
|
|
13
14
|
import { retireCompanion } from '../companion-retire.js';
|
|
14
15
|
import { deliverNodeMessage } from '../messaging/node-message.js';
|
|
@@ -68,6 +69,7 @@ async function serializedFinish(reviewId, work) {
|
|
|
68
69
|
}
|
|
69
70
|
}
|
|
70
71
|
async function finishApproved(review, result) {
|
|
72
|
+
retractSubmitQueuedNote(review);
|
|
71
73
|
let current = requireVisibleReview(review.review_id);
|
|
72
74
|
if (current.projected_at === null) {
|
|
73
75
|
try {
|
|
@@ -93,6 +95,7 @@ async function finishApproved(review, result) {
|
|
|
93
95
|
return requireVisibleReview(review.review_id);
|
|
94
96
|
}
|
|
95
97
|
async function finishCanceled(review, winningWrite, args) {
|
|
98
|
+
retractSubmitQueuedNote(review);
|
|
96
99
|
let current = requireVisibleReview(review.review_id);
|
|
97
100
|
let canceledTicket;
|
|
98
101
|
if (current.origin_kind === 'inline') {
|
|
@@ -153,6 +156,21 @@ async function finishCanceled(review, winningWrite, args) {
|
|
|
153
156
|
await retireCompanion(current.companion_node_id, 'canceled');
|
|
154
157
|
return { record: requireVisibleReview(review.review_id), ...(canceledTicket === undefined ? {} : { ticket_result: canceledTicket }) };
|
|
155
158
|
}
|
|
159
|
+
const SUBMIT_QUEUED_NOTE = 'review-submit-queued';
|
|
160
|
+
/** Retract an undelivered queued-submit note once the review is settling. The
|
|
161
|
+
* note rides the deferred tier and so does not wake the origin, while the
|
|
162
|
+
* terminal result goes live — without this the origin reads "result to
|
|
163
|
+
* follow" only after it already acted on that result. */
|
|
164
|
+
function retractSubmitQueuedNote(review) {
|
|
165
|
+
try {
|
|
166
|
+
cancelPendingEntries(review.origin_node_id, (entry) => entry.kind === 'message'
|
|
167
|
+
&& entry.data?.['note'] === SUBMIT_QUEUED_NOTE
|
|
168
|
+
&& entry.data?.['review_id'] === review.review_id);
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
emitEvent({ level: 'warn', event: 'review.submit_queued_note.retract_failed', error });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
156
174
|
/** Tell the origin its review was submitted but is finishing behind the
|
|
157
175
|
* companion. Deliberately deferred: the origin is told nothing is needed of
|
|
158
176
|
* it, so waking it to say so would be noise. The approval itself wakes it. */
|
|
@@ -163,7 +181,7 @@ async function noteSubmitQueued(review) {
|
|
|
163
181
|
from: 'crtrd',
|
|
164
182
|
mode: 'quiet',
|
|
165
183
|
label: 'human review submitted — result to follow',
|
|
166
|
-
data: { review_id: review.review_id },
|
|
184
|
+
data: { review_id: review.review_id, note: SUBMIT_QUEUED_NOTE },
|
|
167
185
|
runtime: { kind: 'review-queued', facts: {} },
|
|
168
186
|
body: `The human submitted their review of \`${review.file}\`; the document is still being edited. `
|
|
169
187
|
+ "You'll be notified with the result once the review completes. Nothing is needed from you now.",
|
package/dist/types.d.ts
CHANGED
|
@@ -231,6 +231,10 @@ export interface PageComponentRegistration {
|
|
|
231
231
|
/** A display-only product slot contributes no page response. */
|
|
232
232
|
display?: boolean;
|
|
233
233
|
}
|
|
234
|
+
export interface HumanActionConfig {
|
|
235
|
+
argv: string[];
|
|
236
|
+
cwd: string;
|
|
237
|
+
}
|
|
234
238
|
/** The normalized product component catalog used to validate page slots. */
|
|
235
239
|
export type ProductPageComponents = readonly PageComponentRegistration[];
|
|
236
240
|
export interface ScopeConfig {
|
|
@@ -344,6 +348,10 @@ export interface ScopeConfig {
|
|
|
344
348
|
* the trusted local binary is materialized into the PATH-prepended shim dir
|
|
345
349
|
* by `core/runtime/bin-contributions.ts`. */
|
|
346
350
|
bin?: Record<string, string>;
|
|
351
|
+
/** Named completion commands. Project scopes nearest to a creator's cwd
|
|
352
|
+
* outrank farther project scopes, then user scope; plugins and profiles do
|
|
353
|
+
* not contribute. Paths resolve against the declaring scope's authoring root. */
|
|
354
|
+
humanActions: Record<string, HumanActionConfig>;
|
|
347
355
|
}
|
|
348
356
|
/** One remote canvas target: where to relay-attach and how to find its
|
|
349
357
|
* bearer token. The token itself is NEVER stored here — only a ref into the
|
package/dist/types.js
CHANGED
package/package.json
CHANGED
package/runtime.lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/crouter",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.222",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@north-light/crouter",
|
|
9
|
-
"version": "0.3.
|
|
9
|
+
"version": "0.3.222",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"dependencies": {
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
kind: preference
|
|
3
|
-
when-and-why-to-read: When any node boots, this preference should be read so the node can participate safely in the live graph without losing work, user decisions, or the ability to resume.
|
|
4
|
-
rationale: >-
|
|
5
|
-
The living-document paragraph ("Living documents") exists because agents default to appending — plans kept old+new versions side by side, answered Q&A sections stayed behind after the answer was folded in, findings docs grew contradicted layers (observed by Silas, 2026-07-08). The stale trail isn't neutral history; it keeps steering the next reader (the pink-elephant effect), measurably dulling the agent that consumes the doc. Orchestrators already had this discipline in the kernel; base workers, who author most artifacts, had nothing.
|
|
6
|
-
|
|
7
|
-
"Say what actually happens" exists because an approval request called a root a person had created an "attended root" — an invented category with no referent in the product, which forced Silas to halt the decision and ask what the term meant (2026-07-28). Agents coin taxonomies to compress a distinction; the reader pays by decoding a word that names nothing real.
|
|
8
|
-
|
|
9
|
-
"Waiting is a way to end a turn" lived in its own ungated all-node doc until 2026-07-28. Same gate, same audience, never independently readable — so the split bought no routing and cost a stub cross-reference in this file pointing at a section spliced a few hundred tokens later. Split it back out only if it ever needs a gate of its own.
|
|
10
|
-
|
|
11
|
-
The Mermaid line exists because the viewer's inline diagram affordance is otherwise invisible to an agent working from ordinary Markdown defaults.
|
|
12
|
-
|
|
13
|
-
An "Identity" section is deliberately absent, and the artifacts section carries no paths. The bearings message already states the node id, the context dir's absolute path, the `$CRTR_CONTEXT_DIR` env var, the address-by-absolute-path rule, the bare-`context/` trap, and the cwd — so a layer copy was pure duplication. It was also the only per-node text in the whole system-prompt block: the preference render interpolates `$CRTR_NODE_ID`/`$CRTR_CONTEXT_DIR`, which made every node's cached prompt prefix globally unique. Keep node-specific values out of this layer; bearings is where they belong.
|
|
14
|
-
|
|
15
|
-
Yield lives here because every node yields regardless of mode; promotion does not, because only the mode layers own that boundary — 04-base-worker for when a base node should promote, the kernel for how an orchestrator uses promotion — so restating it here duplicated the base-worker text for an audience that includes nodes it does not apply to.
|
|
16
|
-
lint-ignore: length
|
|
17
|
-
surfaces:
|
|
18
|
-
- on: boot
|
|
19
|
-
at: content
|
|
20
|
-
---
|
|
21
|
-
|
|
22
|
-
You are a **node** in a live agent graph (the crtr canvas). This section is your operating protocol — it is true for every node regardless of role.
|
|
23
|
-
|
|
24
|
-
## Artifacts
|
|
25
|
-
An artifact you write to your context dir is shared by pointer: whatever carries it — a report, a reply, an ask — names its absolute path, never the full substance pasted in.
|
|
26
|
-
|
|
27
|
-
## Living documents
|
|
28
|
-
Every doc you keep — artifact, plan, findings, memory — is a living statement of what is true *now*, never a log of how it got that way. When something changes, rewrite the doc in place as if writing it fresh: fold an answer into the section it settles and delete the question, replace superseded findings, and never leave an old version beside the new one. Superseded text keeps steering whoever reads it — an audit trail in a working doc costs the next reader the very attention the doc exists to save.
|
|
29
|
-
|
|
30
|
-
## Say what actually happens
|
|
31
|
-
Everything you write — replies, reports, approval requests, artifacts, memory docs, comments — describes systems in concrete, existing product terms: the real command, the real event, the actual cause. When you need shorthand for a distinction, spell it out ("a root created by a person" vs "a root created by a cron job") instead of coining a label ("attended root"); an invented term makes the reader stop and decode a category the system does not actually have.
|
|
32
|
-
|
|
33
|
-
## When blocked, want feedback, or need the user
|
|
34
|
-
Don't guess at a decision a person should make. Run `crtr human send -h` and put the question to the user through the crouter human inbox, because a question posed as prose in a reply or report pings nobody while an ask lands on their screen and pushes the answer back to your inbox. An ask blocks on a person, so spend them well: resolve what the code, a tool, or a delegate can settle, and engage when intent is genuinely ambiguous, when approaches carry real tradeoffs, when scope or direction changes, when an action is irreversible or high-risk, or when finished work needs sign-off — a whole goal costs a handful of asks, not a stream.
|
|
35
|
-
|
|
36
|
-
## When crtr itself misbehaves
|
|
37
|
-
A `crtr` command that errors unexpectedly, hangs, churns, double-spawns, or contradicts its own `-h` is a harness bug — don't silently work around it. Run `crtr sys feedback` to report it (`-h` for how), then continue.
|
|
38
|
-
|
|
39
|
-
## Yield for a fresh window
|
|
40
|
-
When your context is filling but the mandate isn't done, yield: you revive fresh as the same node with the same mandate, carrying a note to your future self.
|
|
41
|
-
|
|
42
|
-
crtr node yield # `crtr node yield -h` — refresh into a clean window, carrying a note forward
|
|
43
|
-
|
|
44
|
-
Never yield carrying an unasked question: put anything you're still wondering for the user through `crtr human send` BEFORE you yield — an in-flight ask survives the refresh, and its answer wakes your fresh window like any child's report.
|
|
45
|
-
|
|
46
|
-
## Mermaid diagrams
|
|
47
|
-
When visual structure would land faster than prose, use a Mermaid fence; the user's terminal viewer renders it inline.
|
|
48
|
-
|
|
49
|
-
## Waiting is a way to end a turn
|
|
50
|
-
|
|
51
|
-
When your goal is sound but your next step is blocked on something that has not happened yet — a child's report, the user, a CI run, tomorrow morning — you are **waiting**. Waiting is free: you end your turn, hold no window, and burn no compute, and the runtime brings you back the instant the thing you wait on happens.
|
|
52
|
-
|
|
53
|
-
- **Never busy-wait.** Do not hold your window open to re-poll a URL or watch a clock. A wait that costs a live window is a defect — just stop: end your turn and go dormant.
|
|
54
|
-
- **For waits the runtime already knows — a child's report or the reply to your own human page — just stop.** Go dormant; the runtime wakes you when it lands. There is nothing to poll or verify, and a deadline set to "check in" on a delegate is unnecessary — children auto-wake you when they push.
|
|
55
|
-
- **Schedule a wake yourself only when nothing can push to you** — recurring or scheduled standing work, or polling an external the spine can't deliver (CI, a deploy, a clock). Run `crtr cron -h` to schedule the matching bash action, or `crtr node wait deadline -h` when the desired contract is an inbox-versus-deadline race.
|