@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
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Durable action-completion delivery state. The terminal ticket is composed
|
|
2
|
+
// before this row exists; this module schedules its already-complete document.
|
|
3
|
+
import { openDb, withCanvasWrite } from './db.js';
|
|
4
|
+
function actionDeliveryFrom(row) {
|
|
5
|
+
return {
|
|
6
|
+
requestId: row.request_id,
|
|
7
|
+
state: row.state,
|
|
8
|
+
attempt: row.attempt,
|
|
9
|
+
argv: JSON.parse(row.argv_json),
|
|
10
|
+
cwd: row.cwd,
|
|
11
|
+
completion: JSON.parse(row.completion_json),
|
|
12
|
+
nextAttemptAt: row.next_attempt_at,
|
|
13
|
+
claimedAt: row.claimed_at,
|
|
14
|
+
claimOwner: row.claim_owner,
|
|
15
|
+
acceptedAt: row.accepted_at,
|
|
16
|
+
permanentFailedAt: row.permanent_failed_at,
|
|
17
|
+
lastFailure: row.last_failure_json === null ? null : JSON.parse(row.last_failure_json),
|
|
18
|
+
createdAt: row.created_at,
|
|
19
|
+
updatedAt: row.updated_at,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/** Insert the one delivery row for a settled action-bound request. Idempotent
|
|
23
|
+
* on requestId: a replayed settlement must not create a second row. */
|
|
24
|
+
export function enqueueActionDelivery(row) {
|
|
25
|
+
const now = Date.now();
|
|
26
|
+
withCanvasWrite((db) => {
|
|
27
|
+
db.prepare(`INSERT INTO human_action_deliveries
|
|
28
|
+
(request_id, state, attempt, argv_json, cwd, completion_json, next_attempt_at, created_at, updated_at)
|
|
29
|
+
VALUES (?, 'pending', 0, ?, ?, ?, ?, ?, ?)
|
|
30
|
+
ON CONFLICT(request_id) DO NOTHING`).run(row.requestId, JSON.stringify(row.argv), row.cwd, JSON.stringify(row.completion), now, now, now);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
/** Delivery state for `GET /v1/human/requests/:id`; null when no row exists. */
|
|
34
|
+
export function readActionDelivery(requestId) {
|
|
35
|
+
const row = openDb()
|
|
36
|
+
.prepare('SELECT * FROM human_action_deliveries WHERE request_id = ?')
|
|
37
|
+
.get(requestId);
|
|
38
|
+
return row === undefined ? null : actionDeliveryFrom(row);
|
|
39
|
+
}
|
|
40
|
+
/** Due pending records in deterministic order for the daemon's serial tick. */
|
|
41
|
+
export function dueActionDeliveries(now) {
|
|
42
|
+
return openDb()
|
|
43
|
+
.prepare(`SELECT * FROM human_action_deliveries
|
|
44
|
+
WHERE state = 'pending' AND next_attempt_at <= ?
|
|
45
|
+
ORDER BY next_attempt_at, created_at, request_id`)
|
|
46
|
+
.all(now).map(actionDeliveryFrom);
|
|
47
|
+
}
|
|
48
|
+
/** Atomically start one due delivery attempt. A null result lost the claim. */
|
|
49
|
+
export function claimActionDelivery(requestId, now, claimOwner) {
|
|
50
|
+
return withCanvasWrite((db) => {
|
|
51
|
+
const claimed = db.prepare(`UPDATE human_action_deliveries
|
|
52
|
+
SET state = 'running', attempt = attempt + 1, claimed_at = ?, claim_owner = ?, updated_at = ?
|
|
53
|
+
WHERE request_id = ? AND state = 'pending' AND next_attempt_at <= ?`).run(now, claimOwner, now, requestId, now);
|
|
54
|
+
if (Number(claimed.changes) === 0)
|
|
55
|
+
return null;
|
|
56
|
+
const row = db.prepare('SELECT * FROM human_action_deliveries WHERE request_id = ?').get(requestId);
|
|
57
|
+
return actionDeliveryFrom(row);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
/** Settle only the still-owned running attempt, so a stale process cannot overwrite a newer claim. */
|
|
61
|
+
export function settleActionDelivery(requestId, attempt, claimOwner, settlement, now) {
|
|
62
|
+
const failure = settlement.state === 'accepted' ? null : JSON.stringify(settlement.failure);
|
|
63
|
+
const result = withCanvasWrite((db) => db.prepare(`UPDATE human_action_deliveries
|
|
64
|
+
SET state = ?, next_attempt_at = ?, claimed_at = NULL, claim_owner = NULL,
|
|
65
|
+
accepted_at = ?, permanent_failed_at = ?, last_failure_json = ?, updated_at = ?
|
|
66
|
+
WHERE request_id = ? AND state = 'running' AND attempt = ? AND claim_owner = ?`).run(settlement.state, settlement.state === 'pending' ? settlement.nextAttemptAt : now, settlement.state === 'accepted' ? now : null, settlement.state === 'permanent_failed' ? now : null, failure, now, requestId, attempt, claimOwner));
|
|
67
|
+
return Number(result.changes) === 1;
|
|
68
|
+
}
|
|
69
|
+
/** A new daemon deliberately redelivers interrupted completions at least once. */
|
|
70
|
+
export function recoverRunningActionDeliveries(now) {
|
|
71
|
+
const result = withCanvasWrite((db) => db.prepare(`UPDATE human_action_deliveries
|
|
72
|
+
SET state = 'pending', next_attempt_at = ?, claimed_at = NULL, claim_owner = NULL, updated_at = ?
|
|
73
|
+
WHERE state = 'running'`).run(now, now));
|
|
74
|
+
return Number(result.changes);
|
|
75
|
+
}
|
package/dist/core/config.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PageComponentRegistration, Scope, ScopeConfig, ScopeState, KindConfig, ModelLaddersConfig, ModelProvider, ModelStrength, ModelRouteConfig, ModelRoutingConfig } from '../types.js';
|
|
1
|
+
import type { PageComponentRegistration, Scope, ScopeConfig, ScopeState, HumanActionConfig, KindConfig, ModelLaddersConfig, ModelProvider, ModelStrength, ModelRouteConfig, ModelRoutingConfig } from '../types.js';
|
|
2
2
|
import { type BindingId } from './keybindings/catalog.js';
|
|
3
3
|
export declare function configPath(scope: Scope): string | null;
|
|
4
4
|
export declare function statePath(scope: Scope): string | null;
|
|
@@ -40,6 +40,18 @@ export declare function binDeclarationIssues(raw: unknown): BinDeclarationIssue[
|
|
|
40
40
|
* manifest's) to name → root-relative path. Invalid entries never become
|
|
41
41
|
* commands; `binDeclarationIssues` keeps them visible to Doctor. */
|
|
42
42
|
export declare function normalizeBin(raw: unknown): Record<string, string>;
|
|
43
|
+
export interface HumanActionDeclarationIssue {
|
|
44
|
+
name: string;
|
|
45
|
+
reason: string;
|
|
46
|
+
block?: true;
|
|
47
|
+
}
|
|
48
|
+
/** Validate `humanActions` declaration shape without touching the filesystem.
|
|
49
|
+
* Filesystem checks happen only when resolving an action or running Doctor. */
|
|
50
|
+
export declare function humanActionDeclarationIssues(raw: unknown): HumanActionDeclarationIssue[];
|
|
51
|
+
/** Normalize valid action declarations for scope-local config inspection.
|
|
52
|
+
* Invalid entries remain available through raw reads so resolution and Doctor
|
|
53
|
+
* can report them rather than silently treating them as absent. */
|
|
54
|
+
export declare function normalizeHumanActions(raw: unknown): Record<string, HumanActionConfig>;
|
|
43
55
|
/** STRICT install-time validation for a plugin-declared `bin` block. Target
|
|
44
56
|
* filesystem state is checked later by Doctor because it can change after
|
|
45
57
|
* install. */
|
package/dist/core/config.js
CHANGED
|
@@ -320,6 +320,55 @@ export function normalizeBin(raw) {
|
|
|
320
320
|
}
|
|
321
321
|
return out;
|
|
322
322
|
}
|
|
323
|
+
const HUMAN_ACTION_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
324
|
+
/** Validate `humanActions` declaration shape without touching the filesystem.
|
|
325
|
+
* Filesystem checks happen only when resolving an action or running Doctor. */
|
|
326
|
+
export function humanActionDeclarationIssues(raw) {
|
|
327
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
328
|
+
return [{ name: 'humanActions', reason: 'humanActions must be a JSON object mapping action names to {argv, cwd}', block: true }];
|
|
329
|
+
}
|
|
330
|
+
const issues = [];
|
|
331
|
+
for (const [name, value] of Object.entries(raw)) {
|
|
332
|
+
const reasons = [];
|
|
333
|
+
if (!HUMAN_ACTION_NAME_PATTERN.test(name)) {
|
|
334
|
+
reasons.push(`action name must match ${HUMAN_ACTION_NAME_PATTERN.source}`);
|
|
335
|
+
}
|
|
336
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
337
|
+
reasons.push('action must be an object with argv and cwd');
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
const action = value;
|
|
341
|
+
const extras = Object.keys(action).filter((key) => key !== 'argv' && key !== 'cwd');
|
|
342
|
+
if (extras.length > 0)
|
|
343
|
+
reasons.push(`action has unsupported field${extras.length === 1 ? '' : 's'}: ${extras.join(', ')}`);
|
|
344
|
+
if (!Array.isArray(action.argv) || action.argv.length === 0 || !action.argv.every((arg) => typeof arg === 'string' && arg.length > 0)) {
|
|
345
|
+
reasons.push('argv must be a non-empty array of non-empty strings');
|
|
346
|
+
}
|
|
347
|
+
if (typeof action.cwd !== 'string' || action.cwd.length === 0) {
|
|
348
|
+
reasons.push('cwd is required and must be a non-empty string');
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
if (reasons.length > 0)
|
|
352
|
+
issues.push({ name, reason: reasons.join('; ') });
|
|
353
|
+
}
|
|
354
|
+
return issues;
|
|
355
|
+
}
|
|
356
|
+
/** Normalize valid action declarations for scope-local config inspection.
|
|
357
|
+
* Invalid entries remain available through raw reads so resolution and Doctor
|
|
358
|
+
* can report them rather than silently treating them as absent. */
|
|
359
|
+
export function normalizeHumanActions(raw) {
|
|
360
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
|
|
361
|
+
return {};
|
|
362
|
+
const invalid = new Set(humanActionDeclarationIssues(raw).map((issue) => issue.name));
|
|
363
|
+
const out = {};
|
|
364
|
+
for (const [name, value] of Object.entries(raw)) {
|
|
365
|
+
if (invalid.has(name))
|
|
366
|
+
continue;
|
|
367
|
+
const action = value;
|
|
368
|
+
out[name] = { argv: [...action.argv], cwd: action.cwd };
|
|
369
|
+
}
|
|
370
|
+
return out;
|
|
371
|
+
}
|
|
323
372
|
/** STRICT install-time validation for a plugin-declared `bin` block. Target
|
|
324
373
|
* filesystem state is checked later by Doctor because it can change after
|
|
325
374
|
* install. */
|
|
@@ -635,7 +684,8 @@ function mergeConfig(partial) {
|
|
|
635
684
|
const remoteCanvas = mergeRemoteCanvas(partial.remoteCanvas);
|
|
636
685
|
const spawnEnv = mergeSpawnEnv(partial.spawnEnv, defaults.spawnEnv?.allow);
|
|
637
686
|
const bin = normalizeBin(partial.bin);
|
|
638
|
-
|
|
687
|
+
const humanActions = normalizeHumanActions(partial.humanActions);
|
|
688
|
+
return { bin, humanActions, schema_version, marketplaces, plugins, auto_update, max_panes_per_window, auto_open_child_viewers, brokerThresholds, completion_bell, working_gerunds, whip_mode, page_components, page_surface, whip_messages, whip_message_mode, mouse_mode_default, live_cycles, condensed_history, bash_tool_purpose, fold_finished_tools, summarize_tool_calls, detailed_tool_recaps, keybindings, tmux_passthrough, modelLadders, ...(Object.keys(modelRoutes).length > 0 ? { modelRoutes } : {}), ...(modelRouting !== undefined ? { modelRouting } : {}), kinds, remoteCanvas, spawnEnv };
|
|
639
689
|
}
|
|
640
690
|
/** Raw (un-defaulted) partial config for one scope, or null if the scope has
|
|
641
691
|
* no root or no config.json. Used by `readMergedLaunchConfig` to layer
|
|
@@ -66,6 +66,12 @@ export declare function readCanceledEntryIds(nodeId: string): Set<InboxEntryId>;
|
|
|
66
66
|
* the interrupt's abort lands right behind, and covered handoffs are never
|
|
67
67
|
* re-delivered anyway. Returns the newly canceled entry ids. */
|
|
68
68
|
export declare function cancelPendingHumanMessages(nodeId: string): InboxEntryId[];
|
|
69
|
+
/** Cancel every not-yet-settled entry the predicate selects. Retraction, not
|
|
70
|
+
* deletion: the append-only inbox is untouched and the sidecar grows. Safe to
|
|
71
|
+
* call late — an entry already past the cursor is not pending, and the watcher
|
|
72
|
+
* re-reads the set at plan time so a cancel racing a buffered entry still
|
|
73
|
+
* lands. */
|
|
74
|
+
export declare function cancelPendingEntries(nodeId: string, select: (entry: InboxEntry) => boolean): InboxEntryId[];
|
|
69
75
|
/** Read and validate the persisted exact cursor identity. */
|
|
70
76
|
export declare function readCursor(nodeId: string): InboxEntryId | undefined;
|
|
71
77
|
/** Persist one validated exact cursor identity (atomic tmp+rename). */
|
package/dist/core/feed/inbox.js
CHANGED
|
@@ -245,11 +245,19 @@ export function readCanceledEntryIds(nodeId) {
|
|
|
245
245
|
* the interrupt's abort lands right behind, and covered handoffs are never
|
|
246
246
|
* re-delivered anyway. Returns the newly canceled entry ids. */
|
|
247
247
|
export function cancelPendingHumanMessages(nodeId) {
|
|
248
|
+
return cancelPendingEntries(nodeId, (e) => e.from === 'human' && e.kind === 'message');
|
|
249
|
+
}
|
|
250
|
+
/** Cancel every not-yet-settled entry the predicate selects. Retraction, not
|
|
251
|
+
* deletion: the append-only inbox is untouched and the sidecar grows. Safe to
|
|
252
|
+
* call late — an entry already past the cursor is not pending, and the watcher
|
|
253
|
+
* re-reads the set at plan time so a cancel racing a buffered entry still
|
|
254
|
+
* lands. */
|
|
255
|
+
export function cancelPendingEntries(nodeId, select) {
|
|
248
256
|
const cursor = readCursor(nodeId);
|
|
249
257
|
const pending = readInboxSince(nodeId, cursor);
|
|
250
258
|
const already = readCanceledEntryIds(nodeId);
|
|
251
259
|
const targets = pending
|
|
252
|
-
.filter((e) =>
|
|
260
|
+
.filter((e) => !already.has(e.entry_id) && select(e))
|
|
253
261
|
.map((e) => e.entry_id);
|
|
254
262
|
if (targets.length === 0)
|
|
255
263
|
return [];
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare const HUMAN_ACTION_NAME_PATTERN: RegExp;
|
|
2
|
+
export interface TicketActionBinding {
|
|
3
|
+
schema: 'crtr.human-action-binding/v1';
|
|
4
|
+
/** Declared action name, matching HUMAN_ACTION_NAME_PATTERN. */
|
|
5
|
+
name: string;
|
|
6
|
+
/** Frozen creation payload; opaque JSON, never inspected. */
|
|
7
|
+
payload: unknown;
|
|
8
|
+
/** The creator's absolute cwd, captured verbatim. */
|
|
9
|
+
creatorCwd: string;
|
|
10
|
+
/** Absolute path of the config file that won resolution. */
|
|
11
|
+
configOrigin: string;
|
|
12
|
+
/** Fully resolved argv; argv[0] is an absolute executable path. */
|
|
13
|
+
argv: string[];
|
|
14
|
+
/** Fully resolved absolute working directory. */
|
|
15
|
+
cwd: string;
|
|
16
|
+
}
|
|
17
|
+
/** Publish the binding before the page exists, so a visible request is never
|
|
18
|
+
* unbound. A second write is a caller bug, not a race to tolerate. */
|
|
19
|
+
export declare function publishTicketActionBinding(dir: string, binding: TicketActionBinding): void;
|
|
20
|
+
/** The frozen binding, or null for a request with no completion action. */
|
|
21
|
+
export declare function readTicketActionBinding(dir: string): TicketActionBinding | null;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// The optional completion-action binding of a durable human request. Written
|
|
2
|
+
// once at creation and never again: `replace` revises the page, so the approved
|
|
3
|
+
// effect must not be reachable from any later write.
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { actionBindingPath, publishJsonExclusive } from './convention.js';
|
|
7
|
+
export const HUMAN_ACTION_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
8
|
+
const bindingSchema = z.object({
|
|
9
|
+
schema: z.literal('crtr.human-action-binding/v1'),
|
|
10
|
+
name: z.string().regex(HUMAN_ACTION_NAME_PATTERN),
|
|
11
|
+
payload: z.unknown(),
|
|
12
|
+
creatorCwd: z.string().min(1),
|
|
13
|
+
configOrigin: z.string().min(1),
|
|
14
|
+
argv: z.array(z.string().min(1)).min(1),
|
|
15
|
+
cwd: z.string().min(1),
|
|
16
|
+
}).strict();
|
|
17
|
+
/** Publish the binding before the page exists, so a visible request is never
|
|
18
|
+
* unbound. A second write is a caller bug, not a race to tolerate. */
|
|
19
|
+
export function publishTicketActionBinding(dir, binding) {
|
|
20
|
+
if (!publishJsonExclusive(actionBindingPath(dir), binding)) {
|
|
21
|
+
throw new Error(`action binding already exists: ${actionBindingPath(dir)}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** The frozen binding, or null for a request with no completion action. */
|
|
25
|
+
export function readTicketActionBinding(dir) {
|
|
26
|
+
let raw;
|
|
27
|
+
try {
|
|
28
|
+
raw = JSON.parse(readFileSync(actionBindingPath(dir), 'utf8'));
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (error.code === 'ENOENT')
|
|
32
|
+
return null;
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
const parsed = bindingSchema.parse(raw);
|
|
36
|
+
// `payload` is optional-by-absence in JSON but required in the type; z.unknown()
|
|
37
|
+
// accepts a missing key, so restore it explicitly as the JSON null the
|
|
38
|
+
// completion document must carry.
|
|
39
|
+
return { ...parsed, payload: raw.payload ?? null };
|
|
40
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { TicketActionBinding } from './action-binding.js';
|
|
2
|
+
import type { PageSource } from './page-schema.js';
|
|
3
|
+
import type { TicketResult } from './types.js';
|
|
4
|
+
export type HumanCompletionEvent = 'answered' | 'dismissed' | 'canceled';
|
|
5
|
+
export interface HumanCompletionV1 {
|
|
6
|
+
schema: 'crtr.human-completion/v1';
|
|
7
|
+
requestId: string;
|
|
8
|
+
event: HumanCompletionEvent;
|
|
9
|
+
settledAt: string;
|
|
10
|
+
/** Present only when `event` is `answered`; absent, never empty, otherwise. */
|
|
11
|
+
responses?: Record<string, Record<string, unknown>>;
|
|
12
|
+
action: {
|
|
13
|
+
name: string;
|
|
14
|
+
payload: unknown;
|
|
15
|
+
};
|
|
16
|
+
provenance: {
|
|
17
|
+
source: PageSource;
|
|
18
|
+
actor?: string;
|
|
19
|
+
};
|
|
20
|
+
/** The lane overwrites this with the real attempt before writing stdin. */
|
|
21
|
+
delivery: {
|
|
22
|
+
attempt: number;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** The one place a cancellation record without `disposition` reads as
|
|
26
|
+
* `canceled`. Every other reader treats both dispositions alike. */
|
|
27
|
+
export declare function completionEventFor(result: TicketResult): HumanCompletionEvent;
|
|
28
|
+
/** Build the complete document with `delivery.attempt` as the lane's placeholder. */
|
|
29
|
+
export declare function composeHumanCompletion(args: {
|
|
30
|
+
requestId: string;
|
|
31
|
+
result: PageTerminalResult;
|
|
32
|
+
binding: TicketActionBinding;
|
|
33
|
+
source: PageSource;
|
|
34
|
+
}): HumanCompletionV1;
|
|
35
|
+
/** A review result can never carry an action binding; the composer is total over the rest. */
|
|
36
|
+
export type PageTerminalResult = Extract<TicketResult, {
|
|
37
|
+
kind: 'page' | 'canceled';
|
|
38
|
+
}>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// The `crtr.human-completion/v1` document handed to a completion action on
|
|
2
|
+
// stdin. Composed at settlement from the canonical terminal result, so the
|
|
3
|
+
// delivered document reflects the settlement instant and the delivery lane
|
|
4
|
+
// never reads ticket storage.
|
|
5
|
+
/** The one place a cancellation record without `disposition` reads as
|
|
6
|
+
* `canceled`. Every other reader treats both dispositions alike. */
|
|
7
|
+
export function completionEventFor(result) {
|
|
8
|
+
if (result.kind === 'canceled')
|
|
9
|
+
return result.disposition ?? 'canceled';
|
|
10
|
+
return 'answered';
|
|
11
|
+
}
|
|
12
|
+
/** Build the complete document with `delivery.attempt` as the lane's placeholder. */
|
|
13
|
+
export function composeHumanCompletion(args) {
|
|
14
|
+
const { requestId, result, binding, source } = args;
|
|
15
|
+
const event = completionEventFor(result);
|
|
16
|
+
const actor = result.actor;
|
|
17
|
+
return {
|
|
18
|
+
schema: 'crtr.human-completion/v1',
|
|
19
|
+
requestId,
|
|
20
|
+
event,
|
|
21
|
+
settledAt: result.kind === 'page' ? result.completedAt : result.canceledAt,
|
|
22
|
+
...(result.kind === 'page' ? { responses: result.responses } : {}),
|
|
23
|
+
action: { name: binding.name, payload: binding.payload },
|
|
24
|
+
provenance: { source, ...(actor === undefined ? {} : { actor }) },
|
|
25
|
+
delivery: { attempt: 0 },
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -8,6 +8,8 @@ export declare function responsePath(dir: string): string;
|
|
|
8
8
|
export declare function progressPath(dir: string): string;
|
|
9
9
|
export declare function claimPath(dir: string): string;
|
|
10
10
|
export declare function replyRoutePath(dir: string): string;
|
|
11
|
+
/** The frozen completion-action binding, written once at request creation. */
|
|
12
|
+
export declare function actionBindingPath(dir: string): string;
|
|
11
13
|
export interface TicketReplyRoute {
|
|
12
14
|
bridge_node_id: string;
|
|
13
15
|
}
|
|
@@ -10,6 +10,8 @@ export function responsePath(dir) { return `${dir}/response.json`; }
|
|
|
10
10
|
export function progressPath(dir) { return `${dir}/progress.json`; }
|
|
11
11
|
export function claimPath(dir) { return `${dir}/claim.json`; }
|
|
12
12
|
export function replyRoutePath(dir) { return `${dir}/reply-route.json`; }
|
|
13
|
+
/** The frozen completion-action binding, written once at request creation. */
|
|
14
|
+
export function actionBindingPath(dir) { return `${dir}/action.json`; }
|
|
13
15
|
/** A reply route exists only when completing the ticket must finish a human
|
|
14
16
|
* bridge and fan its result back to subscribers. Its absence is the complete
|
|
15
17
|
* representation of a standalone page. */
|
|
@@ -19,9 +19,22 @@ export interface PreparedPage {
|
|
|
19
19
|
module?: string;
|
|
20
20
|
manifest: PageManifest;
|
|
21
21
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
export interface PreparePageSourceOptions {
|
|
23
|
+
/** `jsx` carries .tsx semantics; `html` is a complete document. */
|
|
24
|
+
dialect: 'jsx' | 'html';
|
|
25
|
+
/** The complete authored page text, inline. */
|
|
26
|
+
document: string;
|
|
27
|
+
source?: PageSource;
|
|
28
|
+
productKinds?: ProductPageComponents;
|
|
29
|
+
delivery?: PageDelivery;
|
|
30
|
+
}
|
|
31
|
+
/** Validate one authored page document into publishable form. The single
|
|
32
|
+
* validation path: `preparePage` reads a file and lands here, and a
|
|
33
|
+
* programmatic request supplies the same text inline. */
|
|
34
|
+
export declare function preparePageSource(opts: PreparePageSourceOptions): PreparedPage;
|
|
35
|
+
/** Validate one authored page source file into publishable form. Reads only: a
|
|
36
|
+
* page that fails authoring leaves nothing behind, so a caller that must create
|
|
37
|
+
* a bridge node prepares first and creates it only once the page is known good.
|
|
25
38
|
* The source is read exactly once, so the published document is the same bytes
|
|
26
39
|
* the manifest was derived from even if the file changes underneath. */
|
|
27
40
|
export declare function preparePage(opts: Omit<SubmitPageOptions, 'dir'>): PreparedPage;
|
|
@@ -81,6 +94,8 @@ export interface PageTicket {
|
|
|
81
94
|
export declare function readPageTicket(dir: string): PageTicket;
|
|
82
95
|
export interface CompletePageOptions {
|
|
83
96
|
completedAt?: string;
|
|
97
|
+
/** Recorded when an authorized programmatic writer answered instead of a person. */
|
|
98
|
+
actor?: string;
|
|
84
99
|
}
|
|
85
100
|
export declare function completePage(dir: string, responses: unknown, claimToken: string, opts?: CompletePageOptions): {
|
|
86
101
|
won: boolean;
|
|
@@ -94,11 +109,15 @@ export declare function takeoverAndCompletePage(dir: string, responses: unknown,
|
|
|
94
109
|
won: boolean;
|
|
95
110
|
result: TicketResult;
|
|
96
111
|
};
|
|
97
|
-
|
|
98
|
-
export declare function cancelTicket(dir: string, opts?: {
|
|
112
|
+
export interface CancelTicketOptions {
|
|
99
113
|
reason?: string;
|
|
100
114
|
actor?: string;
|
|
101
|
-
|
|
115
|
+
/** The recipient closing it writes `dismissed`; the requester withdrawing it
|
|
116
|
+
* writes `canceled`. Omitted where the caller is neither. */
|
|
117
|
+
disposition?: 'canceled' | 'dismissed';
|
|
118
|
+
}
|
|
119
|
+
/** Cancellation races finalization but never requires or removes another claim. */
|
|
120
|
+
export declare function cancelTicket(dir: string, opts?: CancelTicketOptions): {
|
|
102
121
|
status: 'canceled' | 'already_resolved';
|
|
103
122
|
result: TicketResult;
|
|
104
123
|
};
|
|
@@ -23,10 +23,10 @@ const feedbackSchema = z.object({ file: z.string().min(1).refine(isAbsolute, 'fi
|
|
|
23
23
|
});
|
|
24
24
|
});
|
|
25
25
|
const slotResponseSchema = z.record(z.string(), z.unknown());
|
|
26
|
-
const pageResultSchema = z.object({ schema: z.literal('crtr.page-response/v1'), kind: z.literal('page'), responses: z.record(z.string(), slotResponseSchema), summary: z.string(), completedAt: iso }).strict();
|
|
26
|
+
const pageResultSchema = z.object({ schema: z.literal('crtr.page-response/v1'), kind: z.literal('page'), responses: z.record(z.string(), slotResponseSchema), summary: z.string(), completedAt: iso, actor: z.string().optional() }).strict();
|
|
27
27
|
const pageProgressSchema = z.object({ kind: z.literal('page'), responses: z.record(z.string(), slotResponseSchema), savedAt: iso }).strict();
|
|
28
28
|
const reviewResultSchema = z.object({ schema: z.literal('humanloop.review-response/v1'), kind: z.literal('review'), result: feedbackSchema, completedAt: iso }).strict();
|
|
29
|
-
const canceledResultSchema = z.object({ schema: z.literal('humanloop.cancel/v1'), kind: z.literal('canceled'), canceledAt: iso, reason: z.string().optional(), actor: z.string().optional() }).strict();
|
|
29
|
+
const canceledResultSchema = z.object({ schema: z.literal('humanloop.cancel/v1'), kind: z.literal('canceled'), canceledAt: iso, reason: z.string().optional(), actor: z.string().optional(), disposition: z.enum(['canceled', 'dismissed']).optional() }).strict();
|
|
30
30
|
/** Strict decoder for the only canonical final marker. */
|
|
31
31
|
export function readTicketResult(dir) {
|
|
32
32
|
let raw;
|
|
@@ -52,9 +52,19 @@ function publishRequest(path, value) {
|
|
|
52
52
|
if (!publishJsonExclusive(path, value))
|
|
53
53
|
throw new Error(`ticket request already exists: ${path}`);
|
|
54
54
|
}
|
|
55
|
-
/** Validate one authored page
|
|
56
|
-
*
|
|
57
|
-
*
|
|
55
|
+
/** Validate one authored page document into publishable form. The single
|
|
56
|
+
* validation path: `preparePage` reads a file and lands here, and a
|
|
57
|
+
* programmatic request supplies the same text inline. */
|
|
58
|
+
export function preparePageSource(opts) {
|
|
59
|
+
const stamped = { ...(opts.source ?? {}), emittedAt: opts.source?.emittedAt ?? new Date().toISOString() };
|
|
60
|
+
const delivery = opts.delivery ?? { placement: 'inline', inbox: false, reply: false };
|
|
61
|
+
if (opts.dialect === 'html')
|
|
62
|
+
return { document: opts.document, manifest: deriveHtmlPageManifest(opts.document, stamped, delivery) };
|
|
63
|
+
return { document: opts.document, module: compilePage(opts.document), manifest: derivePageManifestFromDocument(opts.document, opts.productKinds ?? [], stamped, delivery) };
|
|
64
|
+
}
|
|
65
|
+
/** Validate one authored page source file into publishable form. Reads only: a
|
|
66
|
+
* page that fails authoring leaves nothing behind, so a caller that must create
|
|
67
|
+
* a bridge node prepares first and creates it only once the page is known good.
|
|
58
68
|
* The source is read exactly once, so the published document is the same bytes
|
|
59
69
|
* the manifest was derived from even if the file changes underneath. */
|
|
60
70
|
export function preparePage(opts) {
|
|
@@ -63,12 +73,8 @@ export function preparePage(opts) {
|
|
|
63
73
|
throw new PageAuthoringError(`page source file must be .tsx, .html, or .htm: ${opts.sourceFile}`);
|
|
64
74
|
if (!existsSync(opts.sourceFile))
|
|
65
75
|
throw new PageAuthoringError(`page source file does not exist: ${opts.sourceFile}`);
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
const delivery = opts.delivery ?? { placement: 'inline', inbox: false, reply: false };
|
|
69
|
-
if (html)
|
|
70
|
-
return { document, manifest: deriveHtmlPageManifest(document, stamped, delivery) };
|
|
71
|
-
return { document, module: compilePage(document), manifest: derivePageManifestFromDocument(document, opts.productKinds ?? [], stamped, delivery) };
|
|
76
|
+
const { sourceFile: _sourceFile, ...rest } = opts;
|
|
77
|
+
return preparePageSource({ ...rest, dialect: html ? 'html' : 'jsx', document: readFileSync(opts.sourceFile, 'utf8') });
|
|
72
78
|
}
|
|
73
79
|
/** Remove a stale generation file. The manifest always switches first, so a
|
|
74
80
|
* failed cleanup leaves one valid current record plus harmless old source. */
|
|
@@ -237,7 +243,7 @@ function completePageLocked(dir, responses, claimToken, opts) {
|
|
|
237
243
|
requireClaimOwnership(dir, claimToken);
|
|
238
244
|
const manifest = requirePage(dir);
|
|
239
245
|
const parsedResponses = validateFinalPageTicketResponses(dir, responses);
|
|
240
|
-
const result = { schema: 'crtr.page-response/v1', kind: 'page', responses: parsedResponses, summary: buildPageSummary(manifest, parsedResponses), completedAt: opts.completedAt ?? new Date().toISOString() };
|
|
246
|
+
const result = { schema: 'crtr.page-response/v1', kind: 'page', responses: parsedResponses, summary: buildPageSummary(manifest, parsedResponses), completedAt: opts.completedAt ?? new Date().toISOString(), ...(opts.actor === undefined ? {} : { actor: opts.actor }) };
|
|
241
247
|
const won = exclusiveResult(dir, result);
|
|
242
248
|
clearOwnedWork(dir, claimToken);
|
|
243
249
|
return { won, result: won ? result : readTicketResult(dir) ?? result };
|
|
@@ -280,7 +286,7 @@ export function cancelTicket(dir, opts = {}) {
|
|
|
280
286
|
const canceled = withTicketLock(ticket, () => {
|
|
281
287
|
if (existsSync(pageManifestPath(ticket)))
|
|
282
288
|
requireCancelablePage(ticket);
|
|
283
|
-
const result = { schema: 'humanloop.cancel/v1', kind: 'canceled', canceledAt: new Date().toISOString(), ...(opts.reason === undefined ? {} : { reason: opts.reason }), ...(opts.actor === undefined ? {} : { actor: opts.actor }) };
|
|
289
|
+
const result = { schema: 'humanloop.cancel/v1', kind: 'canceled', canceledAt: new Date().toISOString(), ...(opts.reason === undefined ? {} : { reason: opts.reason }), ...(opts.actor === undefined ? {} : { actor: opts.actor }), ...(opts.disposition === undefined ? {} : { disposition: opts.disposition }) };
|
|
284
290
|
const won = exclusiveResult(ticket, result);
|
|
285
291
|
return { status: won ? 'canceled' : 'already_resolved', result: won ? result : readTicketResult(ticket) ?? result };
|
|
286
292
|
});
|
|
@@ -89,6 +89,8 @@ export interface PageTicketResult {
|
|
|
89
89
|
responses: PageResponses;
|
|
90
90
|
summary: string;
|
|
91
91
|
completedAt: string;
|
|
92
|
+
/** Recorded when an authorized programmatic writer answered instead of a person. */
|
|
93
|
+
actor?: string;
|
|
92
94
|
}
|
|
93
95
|
export interface ReviewTicketResult {
|
|
94
96
|
schema: 'humanloop.review-response/v1';
|
|
@@ -102,6 +104,9 @@ export interface CanceledTicketResult {
|
|
|
102
104
|
canceledAt: string;
|
|
103
105
|
reason?: string;
|
|
104
106
|
actor?: string;
|
|
107
|
+
/** Who ended it without an answer: the recipient (`dismissed`) or the
|
|
108
|
+
* requester (`canceled`). Absent on records written before the field. */
|
|
109
|
+
disposition?: 'canceled' | 'dismissed';
|
|
105
110
|
}
|
|
106
111
|
/** The sole canonical response.json union. */
|
|
107
112
|
export type TicketResult = PageTicketResult | ReviewTicketResult | CanceledTicketResult;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface ResolvedHumanAction {
|
|
2
|
+
name: string;
|
|
3
|
+
/** Absolute path of the config file that won resolution. */
|
|
4
|
+
configOrigin: string;
|
|
5
|
+
/** Fully resolved argv; argv[0] is an absolute executable path. */
|
|
6
|
+
argv: string[];
|
|
7
|
+
/** Fully resolved absolute working directory. */
|
|
8
|
+
cwd: string;
|
|
9
|
+
}
|
|
10
|
+
export type HumanActionIssue = {
|
|
11
|
+
kind: 'unknown_action';
|
|
12
|
+
name: string;
|
|
13
|
+
} | {
|
|
14
|
+
kind: 'invalid_action';
|
|
15
|
+
name: string;
|
|
16
|
+
configOrigin: string;
|
|
17
|
+
reason: string;
|
|
18
|
+
block?: true;
|
|
19
|
+
};
|
|
20
|
+
/** Resolve one declared action from a creator's absolute cwd: nearest project
|
|
21
|
+
* ancestor first, then farther project ancestors, then user scope. Never
|
|
22
|
+
* consults plugin or profile config. Read-only; never executes anything. */
|
|
23
|
+
export declare function resolveHumanAction(name: string, creatorCwd: string): ResolvedHumanAction | HumanActionIssue;
|
|
24
|
+
/** Every declared action reachable from a cwd, with its issues, for doctor. */
|
|
25
|
+
export declare function inspectHumanActions(creatorCwd: string): Array<ResolvedHumanAction | HumanActionIssue>;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { statSync } from 'node:fs';
|
|
2
|
+
import { dirname, join, resolve } from 'node:path';
|
|
3
|
+
import { CONFIG_FILE } from '../types.js';
|
|
4
|
+
import { humanActionDeclarationIssues, readRawConfigAtRoot } from './config.js';
|
|
5
|
+
import { findProjectScopeRoots, scopeRoot } from './scope.js';
|
|
6
|
+
function actionContributors(creatorCwd) {
|
|
7
|
+
const contributors = [];
|
|
8
|
+
for (const scopeRootPath of findProjectScopeRoots(creatorCwd, null)) {
|
|
9
|
+
const raw = readRawConfigAtRoot(scopeRootPath)?.humanActions;
|
|
10
|
+
if (raw !== undefined) {
|
|
11
|
+
contributors.push({
|
|
12
|
+
configOrigin: join(scopeRootPath, CONFIG_FILE),
|
|
13
|
+
authoringRoot: dirname(scopeRootPath),
|
|
14
|
+
raw,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const userRoot = scopeRoot('user');
|
|
19
|
+
if (userRoot !== null) {
|
|
20
|
+
const raw = readRawConfigAtRoot(userRoot)?.humanActions;
|
|
21
|
+
if (raw !== undefined) {
|
|
22
|
+
contributors.push({ configOrigin: join(userRoot, CONFIG_FILE), authoringRoot: userRoot, raw });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return contributors;
|
|
26
|
+
}
|
|
27
|
+
function declarationIssue(name, raw) {
|
|
28
|
+
return humanActionDeclarationIssues({ [name]: raw }).find((issue) => issue.name === name)?.reason;
|
|
29
|
+
}
|
|
30
|
+
function resolveDeclaredAction(name, declaration, contributor) {
|
|
31
|
+
const cwd = resolve(contributor.authoringRoot, declaration.cwd);
|
|
32
|
+
try {
|
|
33
|
+
if (!statSync(cwd).isDirectory()) {
|
|
34
|
+
return { kind: 'invalid_action', name, configOrigin: contributor.configOrigin, reason: `cwd is not a directory: ${cwd}` };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return { kind: 'invalid_action', name, configOrigin: contributor.configOrigin, reason: `cwd does not exist: ${cwd}` };
|
|
39
|
+
}
|
|
40
|
+
const executable = resolve(contributor.authoringRoot, declaration.argv[0]);
|
|
41
|
+
try {
|
|
42
|
+
const stat = statSync(executable);
|
|
43
|
+
if (!stat.isFile()) {
|
|
44
|
+
return { kind: 'invalid_action', name, configOrigin: contributor.configOrigin, reason: `argv[0] is not a regular file: ${executable}` };
|
|
45
|
+
}
|
|
46
|
+
if ((stat.mode & 0o111) === 0) {
|
|
47
|
+
return { kind: 'invalid_action', name, configOrigin: contributor.configOrigin, reason: `argv[0] lacks the POSIX exec bit: ${executable}` };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return { kind: 'invalid_action', name, configOrigin: contributor.configOrigin, reason: `argv[0] does not exist: ${executable}` };
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
name,
|
|
55
|
+
configOrigin: contributor.configOrigin,
|
|
56
|
+
argv: [executable, ...declaration.argv.slice(1)],
|
|
57
|
+
cwd,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/** Resolve one declared action from a creator's absolute cwd: nearest project
|
|
61
|
+
* ancestor first, then farther project ancestors, then user scope. Never
|
|
62
|
+
* consults plugin or profile config. Read-only; never executes anything. */
|
|
63
|
+
export function resolveHumanAction(name, creatorCwd) {
|
|
64
|
+
for (const contributor of actionContributors(creatorCwd)) {
|
|
65
|
+
if (contributor.raw === null || typeof contributor.raw !== 'object' || Array.isArray(contributor.raw))
|
|
66
|
+
continue;
|
|
67
|
+
const declarations = contributor.raw;
|
|
68
|
+
if (!Object.hasOwn(declarations, name))
|
|
69
|
+
continue;
|
|
70
|
+
const raw = declarations[name];
|
|
71
|
+
const issue = declarationIssue(name, raw);
|
|
72
|
+
if (issue !== undefined) {
|
|
73
|
+
return { kind: 'invalid_action', name, configOrigin: contributor.configOrigin, reason: issue };
|
|
74
|
+
}
|
|
75
|
+
return resolveDeclaredAction(name, raw, contributor);
|
|
76
|
+
}
|
|
77
|
+
return { kind: 'unknown_action', name };
|
|
78
|
+
}
|
|
79
|
+
/** Every declared action reachable from a cwd, with its issues, for doctor. */
|
|
80
|
+
export function inspectHumanActions(creatorCwd) {
|
|
81
|
+
const results = [];
|
|
82
|
+
for (const contributor of actionContributors(creatorCwd)) {
|
|
83
|
+
const issues = humanActionDeclarationIssues(contributor.raw);
|
|
84
|
+
const invalid = new Map(issues.map((issue) => [issue.name, issue.reason]));
|
|
85
|
+
if (contributor.raw === null || typeof contributor.raw !== 'object' || Array.isArray(contributor.raw)) {
|
|
86
|
+
for (const issue of issues) {
|
|
87
|
+
results.push({ kind: 'invalid_action', name: issue.name, configOrigin: contributor.configOrigin, reason: issue.reason, ...(issue.block === true ? { block: true } : {}) });
|
|
88
|
+
}
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
for (const [name, raw] of Object.entries(contributor.raw)) {
|
|
92
|
+
const issue = invalid.get(name);
|
|
93
|
+
if (issue !== undefined) {
|
|
94
|
+
results.push({ kind: 'invalid_action', name, configOrigin: contributor.configOrigin, reason: issue });
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
results.push(resolveDeclaredAction(name, raw, contributor));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return results;
|
|
101
|
+
}
|
|
@@ -313,7 +313,7 @@ function matchNormalizedChild(dir, segment, want, ext = '.md') {
|
|
|
313
313
|
* directory by normalized name, then resolve the final segment as either a
|
|
314
314
|
* `.md` file or a directory. Either half is null when
|
|
315
315
|
* the segment path does not resolve that way. This is what makes
|
|
316
|
-
* `00-runtime-base.md` findable as `runtime-base` and `01-spine/00-has-manager`
|
|
316
|
+
* `00-runtime-base/00-authoring.md` findable as `runtime-base/authoring` and `01-spine/00-has-manager`
|
|
317
317
|
* findable as `spine/has-manager` — the physical path keeps its pins, only
|
|
318
318
|
* lookup is prefix-blind. */
|
|
319
319
|
function resolveNormalizedPath(baseDir, segments, ext = '.md') {
|