@danypops/papyrus 0.44.4 → 0.44.6
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/package.json +3 -3
- package/src/adapters/sqlite-artifact-store.ts +5 -0
- package/src/cli.ts +15 -17
- package/src/constants.ts +3 -1
- package/src/daemon-state.ts +16 -1
- package/src/daemon.ts +10 -1
- package/src/db.ts +30 -0
- package/src/domain/artifact-alias.ts +36 -0
- package/src/domain/artifact.ts +5 -0
- package/src/ops.ts +30 -3
- package/src/ports/artifact-store.ts +2 -0
- package/src/vehicle/artifact-vehicle-shared.ts +9 -11
- package/src/vehicle/discuss-vehicle.ts +11 -11
- package/src/vehicle/docs-vehicle.ts +2 -1
- package/src/vehicle/notes-vehicle.ts +8 -8
- package/src/vehicle/playbooks-vehicle.ts +1 -1
- package/src/vehicle/rules-vehicle.ts +2 -1
- package/src/vehicle/tasks-vehicle.ts +49 -25
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danypops/papyrus",
|
|
3
|
-
"version": "0.44.
|
|
3
|
+
"version": "0.44.6",
|
|
4
4
|
"description": "Daemon-backed graph artifacts, evidence-bearing tasks, rules, skills, and native TUI workflows for Pi",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": ["pi-package"],
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"files": ["src", "README.md"],
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@danypops/vehicle-client": "^0.6.1",
|
|
38
|
-
"@danypops/vehicle-core": "^0.
|
|
39
|
-
"@danypops/vehicle-server": "^0.
|
|
38
|
+
"@danypops/vehicle-core": "^0.12.3",
|
|
39
|
+
"@danypops/vehicle-server": "^0.18.1"
|
|
40
40
|
}
|
|
41
41
|
}
|
|
@@ -15,6 +15,7 @@ import type { ArtifactTrashRecord } from "../domain/artifact-trash.ts";
|
|
|
15
15
|
import {
|
|
16
16
|
createArtifact,
|
|
17
17
|
getArtifact,
|
|
18
|
+
getArtifactByAlias,
|
|
18
19
|
getArtifactTrash,
|
|
19
20
|
linkArtifacts,
|
|
20
21
|
listArtifactTrash,
|
|
@@ -47,6 +48,10 @@ export class SQLiteArtifactStore implements AtomicArtifactStore, ArtifactTrashSt
|
|
|
47
48
|
return getArtifact(this.db, id, options);
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
getByAlias(alias: string): Artifact | null {
|
|
52
|
+
return getArtifactByAlias(this.db, alias);
|
|
53
|
+
}
|
|
54
|
+
|
|
50
55
|
query(filter: ArtifactQuery): Artifact[] {
|
|
51
56
|
return queryArtifacts(this.db, filter);
|
|
52
57
|
}
|
package/src/cli.ts
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
3
|
import { copyFileSync, existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
|
-
import { homedir } from "node:os";
|
|
5
|
-
import { join } from "node:path";
|
|
6
4
|
import { fileURLToPath } from "node:url";
|
|
7
5
|
import { createNodeServiceInstallDeps, generateSystemdUnit, installUserService, type ServiceSpec } from "@danypops/vehicle-server/service";
|
|
8
6
|
import { connectPapyrusClient, type PapyrusClient } from "./client.ts";
|
|
9
7
|
import { DAEMON_UNIT_NAME, dbPath, TASK_EXECUTION_MAX_NODES } from "./constants.ts";
|
|
10
8
|
import { serveMain } from "./daemon.ts";
|
|
9
|
+
import { daemonStateDir, vehicleHandlePath } from "./daemon-state.ts";
|
|
11
10
|
import { openDb } from "./db.ts";
|
|
12
11
|
import type { GateResult } from "./domain/gate.ts";
|
|
13
12
|
import { applyIdMigration, type IdMigrationPlan, mirrorDatabase, planIdMigration, verifyIdMigration } from "./id-migration.ts";
|
|
14
13
|
import type { TaskExecutionPlan } from "./task-execution.ts";
|
|
15
14
|
import type { TaskBlockage, TaskCompletion } from "./task-service.ts";
|
|
15
|
+
import { VERSION } from "./version.ts";
|
|
16
16
|
|
|
17
17
|
export interface SystemdUnitOptions {
|
|
18
18
|
bunBin: string;
|
|
@@ -26,20 +26,23 @@ export interface SystemdUnitOptions {
|
|
|
26
26
|
*
|
|
27
27
|
* Papyrus's own daemon.ts does not (yet) use vehicle-server's startDaemon/runDaemonProcess -- it's
|
|
28
28
|
* a bespoke Bun.serve() with its own state-file layout (daemon-state.ts) and maintenance-timer
|
|
29
|
-
* scheduling, predating that shared substrate.
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* is tracked separately.
|
|
29
|
+
* scheduling, predating that shared substrate. handlePath points at the real {host,port,pid} file
|
|
30
|
+
* daemon.ts writes on startup (writeVehicleHandle), so Armada's readiness probe works once Papyrus
|
|
31
|
+
* is service-installed. DAEMON_KIT_LAUNCH_PROVENANCE=service is emitted (generateSystemdUnit always
|
|
32
|
+
* adds it) but is currently inert -- Papyrus's daemon never reads it, since it has no idle-shutdown
|
|
33
|
+
* concept of its own. Migrating daemon.ts's own substrate onto vehicle-server's shared daemon
|
|
34
|
+
* runtime entirely is tracked separately.
|
|
35
35
|
*/
|
|
36
|
-
function papyrusServiceSpec(options: SystemdUnitOptions): ServiceSpec {
|
|
36
|
+
export function papyrusServiceSpec(options: SystemdUnitOptions): ServiceSpec {
|
|
37
37
|
return {
|
|
38
38
|
name: "papyrus",
|
|
39
39
|
displayName: "Papyrus graph artifact service",
|
|
40
|
+
version: VERSION,
|
|
40
41
|
binPath: options.bunBin,
|
|
41
42
|
args: [options.cliPath, "serve"],
|
|
42
|
-
|
|
43
|
+
// The real {host,port,pid} file daemon.ts writes on startup (writeVehicleHandle) --
|
|
44
|
+
// Armada's readiness probe polls exactly this path once Papyrus is service-installed.
|
|
45
|
+
handlePath: vehicleHandlePath(daemonStateDir()),
|
|
43
46
|
// Restart=always/RestartSec=2 already unconditional in the prior hand-rolled unit --
|
|
44
47
|
// preserved exactly, not a new opt-in.
|
|
45
48
|
restartOnFailure: true,
|
|
@@ -51,11 +54,6 @@ export function renderSystemdUnit(options: SystemdUnitOptions): string {
|
|
|
51
54
|
return generateSystemdUnit(papyrusServiceSpec(options));
|
|
52
55
|
}
|
|
53
56
|
|
|
54
|
-
function unitPath(): string {
|
|
55
|
-
const configHome = process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
|
|
56
|
-
return join(configHome, "systemd", "user", DAEMON_UNIT_NAME);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
57
|
function systemctl(...args: string[]): void {
|
|
60
58
|
execFileSync("systemctl", ["--user", ...args], { stdio: "inherit" });
|
|
61
59
|
}
|
|
@@ -198,7 +196,7 @@ function usage(): never {
|
|
|
198
196
|
|
|
199
197
|
type TaskCliClient = Pick<PapyrusClient, "call">;
|
|
200
198
|
type MigrationResult = { from: number; to: number; applied: string[] };
|
|
201
|
-
type CliArtifact = { id: string; title: string; status: string; body?: string };
|
|
199
|
+
type CliArtifact = { id: string; alias: string; title: string; status: string; body?: string };
|
|
202
200
|
type CliTaskLease = {
|
|
203
201
|
taskId: string;
|
|
204
202
|
owner: string;
|
|
@@ -241,7 +239,7 @@ function parseJsonAnyFlag(value: string | undefined, flag: string): unknown {
|
|
|
241
239
|
}
|
|
242
240
|
|
|
243
241
|
function artifactLabel(artifact: CliArtifact): string {
|
|
244
|
-
return `${artifact.
|
|
242
|
+
return `${artifact.alias} ${artifact.title}`;
|
|
245
243
|
}
|
|
246
244
|
|
|
247
245
|
function planText(plan: TaskExecutionPlan): string {
|
package/src/constants.ts
CHANGED
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
export const DAEMON_HOST = "127.0.0.1";
|
|
3
3
|
export const DAEMON_PORT_FILE = "port";
|
|
4
4
|
export const DAEMON_TOKEN_FILE = "token";
|
|
5
|
+
/** vehicle-server's own {host,port,pid} handle format -- read by Armada's readiness probe once Papyrus is service-installed, see cli.ts's papyrusServiceSpec. */
|
|
6
|
+
export const DAEMON_HANDLE_FILE = "vehicle-handle.json";
|
|
5
7
|
export const DAEMON_CLIENT_TIMEOUT_MS = 15_000;
|
|
6
8
|
export const DAEMON_PROBE_TIMEOUT_MS = 800;
|
|
7
9
|
export const DAEMON_UNIT_NAME = "papyrus.service";
|
|
8
10
|
export const DAEMON_DIR_ENV = "PAPYRUS_DAEMON_DIR";
|
|
9
11
|
export const SQLITE_BUSY_TIMEOUT_MS = 5_000;
|
|
10
|
-
export const SQLITE_SCHEMA_VERSION =
|
|
12
|
+
export const SQLITE_SCHEMA_VERSION = 24;
|
|
11
13
|
export const SERVICE_MAX_BODY_BYTES = 1_048_576;
|
|
12
14
|
|
|
13
15
|
export const WAL_CHECKPOINT_INTERVAL_MS = 60_000;
|
package/src/daemon-state.ts
CHANGED
|
@@ -2,7 +2,8 @@ import { randomBytes } from "node:crypto";
|
|
|
2
2
|
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import {
|
|
5
|
+
import { LOOPBACK_HOST, removeDaemonHandle, writeDaemonHandle } from "@danypops/vehicle-server/paths";
|
|
6
|
+
import { DAEMON_DIR_ENV, DAEMON_HANDLE_FILE, DAEMON_HOST, DAEMON_PORT_FILE, DAEMON_TOKEN_FILE } from "./constants.ts";
|
|
6
7
|
|
|
7
8
|
export interface DaemonHandle {
|
|
8
9
|
baseUrl: string;
|
|
@@ -42,6 +43,20 @@ export function clearDaemonPort(dir: string): void {
|
|
|
42
43
|
rmSync(join(dir, DAEMON_PORT_FILE), { force: true });
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
/** Where Armada's readiness probe looks once Papyrus is service-installed -- see cli.ts's papyrusServiceSpec. */
|
|
47
|
+
export function vehicleHandlePath(dir: string): string {
|
|
48
|
+
return join(dir, DAEMON_HANDLE_FILE);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** vehicle-server's own {host,port,pid} handle format, distinct from this file's port/token pair -- Armada's readiness probe (createHandleReadinessProbe) reads exactly this shape. */
|
|
52
|
+
export function writeVehicleHandle(dir: string, port: number, pid: number = process.pid): void {
|
|
53
|
+
writeDaemonHandle(vehicleHandlePath(dir), { host: LOOPBACK_HOST, port, pid });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function clearVehicleHandle(dir: string): void {
|
|
57
|
+
removeDaemonHandle(vehicleHandlePath(dir));
|
|
58
|
+
}
|
|
59
|
+
|
|
45
60
|
export function readDaemonHandle(dir: string): DaemonHandle | undefined {
|
|
46
61
|
try {
|
|
47
62
|
const token = readFileSync(join(dir, DAEMON_TOKEN_FILE), "utf8").trim();
|
package/src/daemon.ts
CHANGED
|
@@ -2,7 +2,14 @@ import { join } from "node:path";
|
|
|
2
2
|
import { acquireDaemonLock, releaseDaemonLock } from "@danypops/vehicle-server/paths";
|
|
3
3
|
import { PushChannel } from "@danypops/vehicle-server/push-channel";
|
|
4
4
|
import { DAEMON_HOST, DB_OPTIMIZE_INTERVAL_MS, dbPath, WAL_CHECKPOINT_INTERVAL_MS } from "./constants.ts";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
clearDaemonPort,
|
|
7
|
+
clearVehicleHandle,
|
|
8
|
+
daemonStateDir,
|
|
9
|
+
loadOrCreateToken,
|
|
10
|
+
writeDaemonPort,
|
|
11
|
+
writeVehicleHandle,
|
|
12
|
+
} from "./daemon-state.ts";
|
|
6
13
|
import { logEvent, vehicleLogger } from "./log.ts";
|
|
7
14
|
import { createApp, createPapyrusService } from "./service.ts";
|
|
8
15
|
|
|
@@ -66,6 +73,7 @@ export function serveMain(): void {
|
|
|
66
73
|
throw new Error("Papyrus daemon failed to bind a listener");
|
|
67
74
|
}
|
|
68
75
|
writeDaemonPort(stateDir, server.port);
|
|
76
|
+
writeVehicleHandle(stateDir, server.port);
|
|
69
77
|
const checkpointTimer = setInterval(() => {
|
|
70
78
|
try {
|
|
71
79
|
service.checkpoint();
|
|
@@ -110,6 +118,7 @@ export function serveMain(): void {
|
|
|
110
118
|
clearInterval(reapFocusTimer);
|
|
111
119
|
clearInterval(purgeTrashTimer);
|
|
112
120
|
clearDaemonPort(stateDir);
|
|
121
|
+
clearVehicleHandle(stateDir);
|
|
113
122
|
releaseDaemonLock(lockPath);
|
|
114
123
|
service.close();
|
|
115
124
|
// .finally() re-throws rather than handling a rejection -- catching it first turns a bare
|
package/src/db.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { createRequire } from "node:module";
|
|
|
3
3
|
import { dirname } from "node:path";
|
|
4
4
|
import { runMigrations, type SqliteMigrationRunner } from "@danypops/vehicle-server/storage";
|
|
5
5
|
import { SQLITE_BUSY_TIMEOUT_MS, SQLITE_SCHEMA_VERSION } from "./constants.ts";
|
|
6
|
+
import { generateUniqueAlias, slugify } from "./domain/artifact-alias.ts";
|
|
6
7
|
|
|
7
8
|
const require_ = createRequire(import.meta.url);
|
|
8
9
|
const IS_BUN = typeof (globalThis as { Bun?: unknown }).Bun !== "undefined";
|
|
@@ -99,8 +100,10 @@ CREATE TABLE IF NOT EXISTS artifacts (
|
|
|
99
100
|
extra TEXT DEFAULT '{}',
|
|
100
101
|
created_at TEXT NOT NULL,
|
|
101
102
|
updated_at TEXT NOT NULL,
|
|
103
|
+
alias TEXT,
|
|
102
104
|
FOREIGN KEY (kind, status) REFERENCES statuses(kind, name)
|
|
103
105
|
);
|
|
106
|
+
CREATE UNIQUE INDEX IF NOT EXISTS artifacts_alias_idx ON artifacts(alias);
|
|
104
107
|
CREATE TABLE IF NOT EXISTS edges (
|
|
105
108
|
from_id TEXT NOT NULL REFERENCES artifacts(id),
|
|
106
109
|
relation TEXT NOT NULL REFERENCES relation_names(name),
|
|
@@ -702,6 +705,33 @@ const FUTURE_MIGRATIONS: ReadonlyArray<PapyrusMigration> = [
|
|
|
702
705
|
`);
|
|
703
706
|
},
|
|
704
707
|
},
|
|
708
|
+
{
|
|
709
|
+
version: 24,
|
|
710
|
+
name: "artifact-aliases",
|
|
711
|
+
// See domain/artifact-alias.ts. A real column + unique index, backfilled here rather than
|
|
712
|
+
// computed lazily on read -- IF NOT EXISTS/guarded column add throughout since a fixture
|
|
713
|
+
// that bootstraps from the current SCHEMA text (which already declares alias) and then only
|
|
714
|
+
// fakes an older user_version to exercise this migration path must not fail with "duplicate
|
|
715
|
+
// column name", the same class of concern version 16's comment covers.
|
|
716
|
+
up: (db) => {
|
|
717
|
+
const existing = new Set((db.prepare("PRAGMA table_info(artifacts)").all() as Array<{ name: string }>).map((row) => row.name));
|
|
718
|
+
if (!existing.has("alias")) db.exec("ALTER TABLE artifacts ADD COLUMN alias TEXT");
|
|
719
|
+
const rows = db.prepare("SELECT id, title FROM artifacts WHERE alias IS NULL ORDER BY created_at, id").all() as Array<{
|
|
720
|
+
id: string;
|
|
721
|
+
title: string;
|
|
722
|
+
}>;
|
|
723
|
+
const taken = new Set(
|
|
724
|
+
(db.prepare("SELECT alias FROM artifacts WHERE alias IS NOT NULL").all() as Array<{ alias: string }>).map((row) => row.alias),
|
|
725
|
+
);
|
|
726
|
+
const update = db.prepare("UPDATE artifacts SET alias = ? WHERE id = ?");
|
|
727
|
+
for (const row of rows) {
|
|
728
|
+
const alias = generateUniqueAlias(slugify(row.title), (candidate) => taken.has(candidate));
|
|
729
|
+
taken.add(alias);
|
|
730
|
+
update.run(alias, row.id);
|
|
731
|
+
}
|
|
732
|
+
db.exec("CREATE UNIQUE INDEX IF NOT EXISTS artifacts_alias_idx ON artifacts(alias)");
|
|
733
|
+
},
|
|
734
|
+
},
|
|
705
735
|
];
|
|
706
736
|
|
|
707
737
|
/**
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A short, globally-unique, human/agent-typeable name for an artifact -- alongside its
|
|
3
|
+
* opaque UUID identity (ops.ts's createArtifact), not instead of it. Derived from title by
|
|
4
|
+
* default so the alias stays recognizable, but title itself is free-form prose and not
|
|
5
|
+
* unique -- this module owns turning that into something safe to type, remember, and index.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const MAX_ALIAS_LENGTH = 50;
|
|
9
|
+
const FALLBACK_BASE = "artifact";
|
|
10
|
+
const ALIAS_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
11
|
+
|
|
12
|
+
/** Lowercased, hyphen-separated, ASCII-alphanumeric-only; never empty (falls back to a generic base). */
|
|
13
|
+
export function slugify(title: string): string {
|
|
14
|
+
const slug = title
|
|
15
|
+
.toLowerCase()
|
|
16
|
+
.replace(/'/g, "")
|
|
17
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
18
|
+
.replace(/^-+|-+$/g, "")
|
|
19
|
+
.slice(0, MAX_ALIAS_LENGTH)
|
|
20
|
+
.replace(/-+$/g, "");
|
|
21
|
+
return slug.length > 0 ? slug : FALLBACK_BASE;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Appends the lowest available "-N" suffix (starting at 2) until `isTaken` reports free. */
|
|
25
|
+
export function generateUniqueAlias(base: string, isTaken: (candidate: string) => boolean): string {
|
|
26
|
+
if (!isTaken(base)) return base;
|
|
27
|
+
for (let suffix = 2; ; suffix++) {
|
|
28
|
+
const candidate = `${base}-${suffix}`;
|
|
29
|
+
if (!isTaken(candidate)) return candidate;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Format an explicit caller-supplied alias must satisfy -- the same shape generateUniqueAlias always produces. */
|
|
34
|
+
export function isValidAlias(alias: string): boolean {
|
|
35
|
+
return alias.length > 0 && alias.length <= MAX_ALIAS_LENGTH && ALIAS_PATTERN.test(alias);
|
|
36
|
+
}
|
package/src/domain/artifact.ts
CHANGED
|
@@ -15,6 +15,8 @@ export interface Artifact {
|
|
|
15
15
|
extra: Record<string, unknown>;
|
|
16
16
|
created_at: string;
|
|
17
17
|
updated_at: string;
|
|
18
|
+
/** Short, globally-unique, human/agent-typeable name -- see domain/artifact-alias.ts. Always present once created; the id remains the true backend identity. */
|
|
19
|
+
alias: string;
|
|
18
20
|
edges?: ArtifactEdge[];
|
|
19
21
|
}
|
|
20
22
|
|
|
@@ -28,12 +30,15 @@ export interface CreateArtifactInput {
|
|
|
28
30
|
id?: string;
|
|
29
31
|
subtype?: string;
|
|
30
32
|
templateId?: string;
|
|
33
|
+
/** Overrides the title-derived auto-generated alias -- must be unique and match isValidAlias's format, or creation throws a real conflict/validation error. */
|
|
34
|
+
alias?: string;
|
|
31
35
|
}
|
|
32
36
|
|
|
33
37
|
export interface UpdateArtifactInput {
|
|
34
38
|
title?: string;
|
|
35
39
|
body?: string;
|
|
36
40
|
labels?: string[];
|
|
41
|
+
alias?: string;
|
|
37
42
|
}
|
|
38
43
|
|
|
39
44
|
export interface ArtifactQuery {
|
package/src/ops.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { ARTIFACT_TRASH_RETENTION_MS, DEFAULT_STATUS_BY_KIND } from "./constants
|
|
|
7
7
|
import type { Db } from "./db.ts";
|
|
8
8
|
import { inTransaction } from "./db.ts";
|
|
9
9
|
import type { Artifact, ArtifactQuery, CreateArtifactInput, UpdateArtifactInput } from "./domain/artifact.ts";
|
|
10
|
+
import { generateUniqueAlias, isValidAlias, slugify } from "./domain/artifact-alias.ts";
|
|
10
11
|
import type { ArtifactTrashRecord } from "./domain/artifact-trash.ts";
|
|
11
12
|
|
|
12
13
|
export type { ArtifactTrashRecord } from "./domain/artifact-trash.ts";
|
|
@@ -131,9 +132,27 @@ function rowToArtifact(row: Record<string, unknown>): Artifact {
|
|
|
131
132
|
extra: JSON.parse((row.extra as string) ?? "{}"),
|
|
132
133
|
created_at: row.created_at as string,
|
|
133
134
|
updated_at: row.updated_at as string,
|
|
135
|
+
alias: row.alias as string,
|
|
134
136
|
};
|
|
135
137
|
}
|
|
136
138
|
|
|
139
|
+
function isAliasTaken(db: Db, alias: string, excludeId?: string): boolean {
|
|
140
|
+
const row = excludeId
|
|
141
|
+
? db.prepare("SELECT 1 FROM artifacts WHERE alias = ? AND id != ?").get(alias, excludeId)
|
|
142
|
+
: db.prepare("SELECT 1 FROM artifacts WHERE alias = ?").get(alias);
|
|
143
|
+
return row != null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Auto-generates a unique alias from title, or validates+reserves an explicit override. Throws a real conflict/validation error rather than silently suffixing a caller-chosen alias. */
|
|
147
|
+
function resolveAlias(db: Db, title: string, explicit: string | undefined, excludeId?: string): string {
|
|
148
|
+
if (explicit === undefined) return generateUniqueAlias(slugify(title), (candidate) => isAliasTaken(db, candidate, excludeId));
|
|
149
|
+
if (!isValidAlias(explicit)) {
|
|
150
|
+
throw new Error(`"${explicit}" is not a valid alias -- lowercase letters, digits, and single hyphens only, max 50 characters`);
|
|
151
|
+
}
|
|
152
|
+
if (isAliasTaken(db, explicit, excludeId)) throw new Error(`alias "${explicit}" is already taken by another artifact`);
|
|
153
|
+
return explicit;
|
|
154
|
+
}
|
|
155
|
+
|
|
137
156
|
/**
|
|
138
157
|
* Appends one immutable row to the generic, kind-agnostic mutation event log.
|
|
139
158
|
* This is the one choke point every ArtifactStore mutation funnels through, so every
|
|
@@ -267,15 +286,21 @@ export function createArtifact(db: Db, input: CreateInput, context?: ArtifactEve
|
|
|
267
286
|
const extra = JSON.stringify(resolved.extra ?? {});
|
|
268
287
|
const subtype = resolved.subtype ?? "";
|
|
269
288
|
inTransaction(db, () => {
|
|
289
|
+
const alias = resolveAlias(db, resolved.title, resolved.alias);
|
|
270
290
|
const stmt = db.prepare(
|
|
271
|
-
"INSERT INTO artifacts (id, kind, title, status, subtype, body, labels, extra, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
291
|
+
"INSERT INTO artifacts (id, kind, title, status, subtype, body, labels, extra, created_at, updated_at, alias) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
272
292
|
);
|
|
273
|
-
stmt.run(id, resolved.kind, resolved.title, status, subtype, resolved.body ?? "", labels, extra, now, now);
|
|
293
|
+
stmt.run(id, resolved.kind, resolved.title, status, subtype, resolved.body ?? "", labels, extra, now, now, alias);
|
|
274
294
|
appendArtifactEvent(db, { artifactId: id, type: "created", toStatus: status, ...context });
|
|
275
295
|
});
|
|
276
296
|
return getArtifact(db, id)!;
|
|
277
297
|
}
|
|
278
298
|
|
|
299
|
+
export function getArtifactByAlias(db: Db, alias: string): Artifact | null {
|
|
300
|
+
const row = db.prepare("SELECT * FROM artifacts WHERE alias = ?").get(alias) as Record<string, unknown> | null;
|
|
301
|
+
return row ? rowToArtifact(row) : null;
|
|
302
|
+
}
|
|
303
|
+
|
|
279
304
|
export function getArtifact(db: Db, id: string, opts?: { tree?: boolean; depth?: number; maxNodes?: number }): Artifact | null {
|
|
280
305
|
const row = db.prepare("SELECT * FROM artifacts WHERE id = ?").get(id) as Record<string, unknown> | null;
|
|
281
306
|
if (!row) return null;
|
|
@@ -517,11 +542,13 @@ export function updateArtifactContent(db: Db, id: string, input: UpdateArtifactI
|
|
|
517
542
|
if (!artifact) return null;
|
|
518
543
|
const now = new Date().toISOString();
|
|
519
544
|
inTransaction(db, () => {
|
|
520
|
-
|
|
545
|
+
const alias = input.alias === undefined ? artifact.alias : resolveAlias(db, input.title ?? artifact.title, input.alias, id);
|
|
546
|
+
db.prepare("UPDATE artifacts SET title = ?, body = ?, labels = ?, updated_at = ?, alias = ? WHERE id = ?").run(
|
|
521
547
|
input.title ?? artifact.title,
|
|
522
548
|
input.body ?? artifact.body,
|
|
523
549
|
JSON.stringify(input.labels ?? artifact.labels),
|
|
524
550
|
now,
|
|
551
|
+
alias,
|
|
525
552
|
id,
|
|
526
553
|
);
|
|
527
554
|
appendArtifactEvent(db, { artifactId: id, type: "updated", ...context });
|
|
@@ -16,6 +16,8 @@ import type { ArtifactEventContext } from "../domain/artifact-event.ts";
|
|
|
16
16
|
export interface ArtifactStore {
|
|
17
17
|
create(input: CreateArtifactInput, context?: ArtifactEventContext): Artifact;
|
|
18
18
|
get(id: string, options?: ArtifactGraphOptions): Artifact | null;
|
|
19
|
+
/** Exact, globally-unique lookup by alias (domain/artifact-alias.ts) -- unlike title, never ambiguous, never scoped. */
|
|
20
|
+
getByAlias(alias: string): Artifact | null;
|
|
19
21
|
query(filter: ArtifactQuery): Artifact[];
|
|
20
22
|
link(link: ArtifactLink, context?: ArtifactEventContext): void;
|
|
21
23
|
/** Idempotent: removing an already-absent relationship is a no-op that returns false, not an error. */
|
|
@@ -146,7 +146,7 @@ export function matchArtifactByName(candidates: readonly Artifact[], name: strin
|
|
|
146
146
|
if (matches.length > 1) {
|
|
147
147
|
throw new VehicleError(
|
|
148
148
|
"artifact-name-ambiguous",
|
|
149
|
-
`${matches.length} artifacts are named "${name}": ${matches.map((a) => `${a.title} (${a.
|
|
149
|
+
`${matches.length} artifacts are named "${name}": ${matches.map((a) => `${a.title} (${a.alias})`).join(", ")} -- use id or alias to disambiguate`,
|
|
150
150
|
{ category: "conflict" },
|
|
151
151
|
);
|
|
152
152
|
}
|
|
@@ -154,16 +154,21 @@ export function matchArtifactByName(candidates: readonly Artifact[], name: strin
|
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
/**
|
|
157
|
-
* Resolves a name to an id
|
|
157
|
+
* Resolves a name to an id. Checks `artifacts.getByAlias` first -- a real, indexed,
|
|
158
|
+
* globally-unique match, unlike title -- before falling back to today's scoped
|
|
159
|
+
* title-based matching, retrying against `fetchWidened` (an unscoped/cross-project
|
|
158
160
|
* search) only when `fetchCandidates` finds nothing. Owns the match-or-widen control
|
|
159
161
|
* flow only -- the caller supplies its own scoped/widened list calls, since scoping
|
|
160
162
|
* differs per domain. Omit `fetchWidened` when there is no wider scope to retry.
|
|
161
163
|
*/
|
|
162
164
|
export function resolveArtifactIdWidened(
|
|
165
|
+
artifacts: ArtifactStore,
|
|
163
166
|
name: string,
|
|
164
167
|
fetchCandidates: () => readonly Artifact[],
|
|
165
168
|
fetchWidened?: () => readonly Artifact[],
|
|
166
169
|
): string {
|
|
170
|
+
const byAlias = artifacts.getByAlias(name.trim());
|
|
171
|
+
if (byAlias) return byAlias.id;
|
|
167
172
|
try {
|
|
168
173
|
return matchArtifactByName(fetchCandidates(), name);
|
|
169
174
|
} catch (error) {
|
|
@@ -172,18 +177,11 @@ export function resolveArtifactIdWidened(
|
|
|
172
177
|
}
|
|
173
178
|
}
|
|
174
179
|
|
|
175
|
-
/** Synchronous equivalent of pi-papyrus's own artifactLabelsById -- server-side, a direct ArtifactStore.get() replaces the extra RPC round-trip that helper needed client-side.
|
|
180
|
+
/** Synchronous equivalent of pi-papyrus's own artifactLabelsById -- server-side, a direct ArtifactStore.get() replaces the extra RPC round-trip that helper needed client-side. Always suffixes the alias -- a short, meaningful, globally-unique reference, unlike the raw UUID it replaces. */
|
|
176
181
|
export function labelsById(artifacts: ArtifactStore, ids: readonly string[]): Map<string, string> {
|
|
177
182
|
const uniqueIds = [...new Set(ids)];
|
|
178
183
|
const resolved = uniqueIds.map((id) => artifacts.get(id)).filter((artifact): artifact is Artifact => artifact !== null);
|
|
179
|
-
|
|
180
|
-
for (const artifact of resolved) titleCounts.set(artifact.title, (titleCounts.get(artifact.title) ?? 0) + 1);
|
|
181
|
-
return new Map(
|
|
182
|
-
resolved.map((artifact) => [
|
|
183
|
-
artifact.id,
|
|
184
|
-
(titleCounts.get(artifact.title) ?? 0) > 1 ? `${artifact.title} (${artifact.id})` : artifact.title,
|
|
185
|
-
]),
|
|
186
|
-
);
|
|
184
|
+
return new Map(resolved.map((artifact) => [artifact.id, `${artifact.title} (${artifact.alias})`]));
|
|
187
185
|
}
|
|
188
186
|
|
|
189
187
|
export interface WorkflowRunNarrativeInput {
|
|
@@ -49,10 +49,10 @@ function roundsTranscript(rounds: readonly DiscussionRound[]): string {
|
|
|
49
49
|
* so there is no widened retry to attempt -- one unscoped candidate set is
|
|
50
50
|
* the whole search space already.
|
|
51
51
|
*/
|
|
52
|
-
function resolveDiscussionId(discussions: Discussions, id: unknown, name: unknown): string {
|
|
52
|
+
function resolveDiscussionId(artifacts: ArtifactStore, discussions: Discussions, id: unknown, name: unknown): string {
|
|
53
53
|
if (typeof id === "string" && id.length > 0) return id;
|
|
54
54
|
if (typeof name !== "string" || name.length === 0) throw validationError("id or name is required");
|
|
55
|
-
return resolveArtifactIdWidened(name, () => discussions.list({}));
|
|
55
|
+
return resolveArtifactIdWidened(artifacts, name, () => discussions.list({}));
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
/**
|
|
@@ -65,7 +65,7 @@ function resolveDiscussionId(discussions: Discussions, id: unknown, name: unknow
|
|
|
65
65
|
function resolveRealTaskId(artifacts: ArtifactStore, id: unknown, name: unknown): string | undefined {
|
|
66
66
|
if (typeof id === "string" && id.length > 0) return id;
|
|
67
67
|
if (typeof name !== "string" || name.length === 0) return undefined;
|
|
68
|
-
return resolveArtifactIdWidened(name, () => artifacts.query({ kind: "task", excludeSubtype: DISCUSSION_SUBTYPE, text: name }));
|
|
68
|
+
return resolveArtifactIdWidened(artifacts, name, () => artifacts.query({ kind: "task", excludeSubtype: DISCUSSION_SUBTYPE, text: name }));
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
function resolveRealTaskIds(artifacts: ArtifactStore, ids: unknown, names: unknown): string[] | undefined {
|
|
@@ -204,7 +204,7 @@ export function registerDiscussVehicleOperations(registry: VehicleRegistry, disc
|
|
|
204
204
|
(input) => {
|
|
205
205
|
normalizeOptions(input);
|
|
206
206
|
defaultActorToAgent(input);
|
|
207
|
-
return { ...input, id: resolveDiscussionId(discussions, input.id, input.name) };
|
|
207
|
+
return { ...input, id: resolveDiscussionId(artifacts, discussions, input.id, input.name) };
|
|
208
208
|
},
|
|
209
209
|
(raw) => {
|
|
210
210
|
const result = raw as DiscussionAndRounds;
|
|
@@ -221,7 +221,7 @@ export function registerDiscussVehicleOperations(registry: VehicleRegistry, disc
|
|
|
221
221
|
"local-write",
|
|
222
222
|
{ id: stringProp, name: stringProp, reason: stringProp, actor: stringProp, source: stringProp, session_id: stringProp },
|
|
223
223
|
[],
|
|
224
|
-
(input) => ({ ...input, id: resolveDiscussionId(discussions, input.id, input.name) }),
|
|
224
|
+
(input) => ({ ...input, id: resolveDiscussionId(artifacts, discussions, input.id, input.name) }),
|
|
225
225
|
(raw) => {
|
|
226
226
|
const artifact = raw as Artifact;
|
|
227
227
|
return { ...artifact, content: [contentBlock(artifactLine(artifact))] };
|
|
@@ -234,7 +234,7 @@ export function registerDiscussVehicleOperations(registry: VehicleRegistry, disc
|
|
|
234
234
|
"local-write",
|
|
235
235
|
{ id: stringProp, name: stringProp, actor: stringProp, source: stringProp, session_id: stringProp },
|
|
236
236
|
[],
|
|
237
|
-
(input) => ({ ...input, id: resolveDiscussionId(discussions, input.id, input.name) }),
|
|
237
|
+
(input) => ({ ...input, id: resolveDiscussionId(artifacts, discussions, input.id, input.name) }),
|
|
238
238
|
(raw) => {
|
|
239
239
|
const artifact = raw as Artifact;
|
|
240
240
|
return { ...artifact, content: [contentBlock(artifactLine(artifact))] };
|
|
@@ -247,7 +247,7 @@ export function registerDiscussVehicleOperations(registry: VehicleRegistry, disc
|
|
|
247
247
|
"local-write",
|
|
248
248
|
{ id: stringProp, name: stringProp, settlement: stringProp, actor: stringProp, source: stringProp, session_id: stringProp },
|
|
249
249
|
["settlement"],
|
|
250
|
-
(input) => ({ ...input, id: resolveDiscussionId(discussions, input.id, input.name) }),
|
|
250
|
+
(input) => ({ ...input, id: resolveDiscussionId(artifacts, discussions, input.id, input.name) }),
|
|
251
251
|
(raw) => {
|
|
252
252
|
const artifact = raw as Artifact;
|
|
253
253
|
return { ...artifact, content: [contentBlock(artifactLine(artifact))] };
|
|
@@ -269,7 +269,7 @@ export function registerDiscussVehicleOperations(registry: VehicleRegistry, disc
|
|
|
269
269
|
},
|
|
270
270
|
[],
|
|
271
271
|
(input) => {
|
|
272
|
-
const discussionId = resolveDiscussionId(discussions, input.id, input.name);
|
|
272
|
+
const discussionId = resolveDiscussionId(artifacts, discussions, input.id, input.name);
|
|
273
273
|
const taskId = resolveRealTaskId(artifacts, input.task_id, input.task_name);
|
|
274
274
|
if (!taskId) throw validationError("task_id or task_name is required");
|
|
275
275
|
return { ...input, id: discussionId, task_id: taskId };
|
|
@@ -297,7 +297,7 @@ export function registerDiscussVehicleOperations(registry: VehicleRegistry, disc
|
|
|
297
297
|
},
|
|
298
298
|
[],
|
|
299
299
|
(input) => {
|
|
300
|
-
const discussionId = resolveDiscussionId(discussions, input.id, input.name);
|
|
300
|
+
const discussionId = resolveDiscussionId(artifacts, discussions, input.id, input.name);
|
|
301
301
|
const taskId = resolveRealTaskId(artifacts, input.task_id, input.task_name);
|
|
302
302
|
if (!taskId) throw validationError("task_id or task_name is required");
|
|
303
303
|
return { ...input, id: discussionId, task_id: taskId };
|
|
@@ -319,7 +319,7 @@ export function registerDiscussVehicleOperations(registry: VehicleRegistry, disc
|
|
|
319
319
|
"read",
|
|
320
320
|
{ id: stringProp, name: stringProp },
|
|
321
321
|
[],
|
|
322
|
-
(input) => ({ ...input, id: resolveDiscussionId(discussions, input.id, input.name) }),
|
|
322
|
+
(input) => ({ ...input, id: resolveDiscussionId(artifacts, discussions, input.id, input.name) }),
|
|
323
323
|
(raw) => {
|
|
324
324
|
const result = raw as DiscussionAndRounds;
|
|
325
325
|
return { ...result, content: [contentBlock(`${artifactLine(result.discussion)}\n\n${roundsTranscript(result.rounds)}`)] };
|
|
@@ -332,7 +332,7 @@ export function registerDiscussVehicleOperations(registry: VehicleRegistry, disc
|
|
|
332
332
|
"read",
|
|
333
333
|
{ id: stringProp, name: stringProp, after_round: numberProp, limit: numberProp },
|
|
334
334
|
[],
|
|
335
|
-
(input) => ({ ...input, id: resolveDiscussionId(discussions, input.id, input.name) }),
|
|
335
|
+
(input) => ({ ...input, id: resolveDiscussionId(artifacts, discussions, input.id, input.name) }),
|
|
336
336
|
(raw) => {
|
|
337
337
|
const rounds = raw as DiscussionRound[];
|
|
338
338
|
return { rounds, content: [contentBlock(roundsTranscript(rounds))] };
|
|
@@ -32,6 +32,7 @@ function resolveDocId(
|
|
|
32
32
|
if (typeof id === "string" && id.length > 0) return id;
|
|
33
33
|
if (typeof name !== "string" || name.length === 0) throw validationError("id or name is required");
|
|
34
34
|
return resolveArtifactIdWidened(
|
|
35
|
+
artifacts,
|
|
35
36
|
name,
|
|
36
37
|
() => listDocuments(artifacts, scopes, { text: name, projectRoot }),
|
|
37
38
|
projectRoot === undefined ? undefined : () => listDocuments(artifacts, scopes, { text: name }),
|
|
@@ -42,7 +43,7 @@ function resolveDocId(
|
|
|
42
43
|
function resolveTargetId(artifacts: ArtifactStore, id: unknown, name: unknown): string {
|
|
43
44
|
if (typeof id === "string" && id.length > 0) return id;
|
|
44
45
|
if (typeof name !== "string" || name.length === 0) throw validationError("target_id or target_name is required");
|
|
45
|
-
return resolveArtifactIdWidened(name, () => artifacts.query({ text: name }));
|
|
46
|
+
return resolveArtifactIdWidened(artifacts, name, () => artifacts.query({ text: name }));
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
export function registerDocsVehicleOperations(
|
|
@@ -26,17 +26,17 @@ const OWNER = "notes";
|
|
|
26
26
|
const LIMITS = { defaultTimeoutMs: 5_000, maxTimeoutMs: 30_000, maxRequestBytes: 65_536, maxResponseBytes: 262_144 };
|
|
27
27
|
|
|
28
28
|
/** Resolves a note's id from either an explicit id or its title within projectRoot. */
|
|
29
|
-
function resolveNoteId(notes: Notes, projectRoot: string, id: unknown, name: unknown): string {
|
|
29
|
+
function resolveNoteId(artifacts: ArtifactStore, notes: Notes, projectRoot: string, id: unknown, name: unknown): string {
|
|
30
30
|
if (typeof id === "string" && id.length > 0) return id;
|
|
31
31
|
if (typeof name !== "string" || name.length === 0) throw validationError("id or name is required");
|
|
32
|
-
return resolveArtifactIdWidened(name, () => notes.list({ projectRoot, text: name }));
|
|
32
|
+
return resolveArtifactIdWidened(artifacts, name, () => notes.list({ projectRoot, text: name }));
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
/** Cross-kind equivalent for a promotion target -- a target can be a task, doc, rule, or playbook, not just a note. Unscoped by project, matching the exact behavior of the artifact.query-backed resolution it replaces. */
|
|
36
36
|
function resolveArtifactId(artifacts: ArtifactStore, id: unknown, name: unknown): string {
|
|
37
37
|
if (typeof id === "string" && id.length > 0) return id;
|
|
38
38
|
if (typeof name !== "string" || name.length === 0) throw validationError("target_id or target_name is required");
|
|
39
|
-
return resolveArtifactIdWidened(name, () => artifacts.query({ text: name }));
|
|
39
|
+
return resolveArtifactIdWidened(artifacts, name, () => artifacts.query({ text: name }));
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
/**
|
|
@@ -100,7 +100,7 @@ export function registerNotesVehicleOperations(registry: VehicleRegistry, notes:
|
|
|
100
100
|
"read",
|
|
101
101
|
{ id: stringProp, name: stringProp, project_root: stringProp },
|
|
102
102
|
["project_root"],
|
|
103
|
-
(input) => ({ ...input, id: resolveNoteId(notes, input.project_root as string, input.id, input.name) }),
|
|
103
|
+
(input) => ({ ...input, id: resolveNoteId(artifacts, notes, input.project_root as string, input.id, input.name) }),
|
|
104
104
|
);
|
|
105
105
|
|
|
106
106
|
define(
|
|
@@ -116,7 +116,7 @@ export function registerNotesVehicleOperations(registry: VehicleRegistry, notes:
|
|
|
116
116
|
direction: { type: "string", enum: ["asc", "desc"] },
|
|
117
117
|
},
|
|
118
118
|
["project_root"],
|
|
119
|
-
(input) => ({ ...input, id: resolveNoteId(notes, input.project_root as string, input.id, input.name) }),
|
|
119
|
+
(input) => ({ ...input, id: resolveNoteId(artifacts, notes, input.project_root as string, input.id, input.name) }),
|
|
120
120
|
);
|
|
121
121
|
|
|
122
122
|
define(
|
|
@@ -133,7 +133,7 @@ export function registerNotesVehicleOperations(registry: VehicleRegistry, notes:
|
|
|
133
133
|
reason: stringProp,
|
|
134
134
|
},
|
|
135
135
|
["project_root"],
|
|
136
|
-
(input) => ({ ...input, id: resolveNoteId(notes, input.project_root as string, input.id, input.name) }),
|
|
136
|
+
(input) => ({ ...input, id: resolveNoteId(artifacts, notes, input.project_root as string, input.id, input.name) }),
|
|
137
137
|
);
|
|
138
138
|
|
|
139
139
|
define(
|
|
@@ -154,7 +154,7 @@ export function registerNotesVehicleOperations(registry: VehicleRegistry, notes:
|
|
|
154
154
|
["project_root"],
|
|
155
155
|
(input) => ({
|
|
156
156
|
...input,
|
|
157
|
-
id: resolveNoteId(notes, input.project_root as string, input.id, input.name),
|
|
157
|
+
id: resolveNoteId(artifacts, notes, input.project_root as string, input.id, input.name),
|
|
158
158
|
target_id: resolveArtifactId(artifacts, input.target_id, input.target_name),
|
|
159
159
|
}),
|
|
160
160
|
);
|
|
@@ -174,6 +174,6 @@ export function registerNotesVehicleOperations(registry: VehicleRegistry, notes:
|
|
|
174
174
|
reason: stringProp,
|
|
175
175
|
},
|
|
176
176
|
["project_root", "disposition"],
|
|
177
|
-
(input) => ({ ...input, id: resolveNoteId(notes, input.project_root as string, input.id, input.name) }),
|
|
177
|
+
(input) => ({ ...input, id: resolveNoteId(artifacts, notes, input.project_root as string, input.id, input.name) }),
|
|
178
178
|
);
|
|
179
179
|
}
|
|
@@ -59,7 +59,7 @@ export interface PlaybooksVehicleDeps {
|
|
|
59
59
|
function resolvePlaybookId(artifacts: ArtifactStore, scopes: ArtifactScopeStore, id: unknown, name: unknown): string {
|
|
60
60
|
if (typeof id === "string" && id.length > 0) return id;
|
|
61
61
|
if (typeof name !== "string" || name.length === 0) throw validationError("id or name is required");
|
|
62
|
-
return resolveArtifactIdWidened(name, () => listPlaybooks(artifacts, scopes, { text: name }));
|
|
62
|
+
return resolveArtifactIdWidened(artifacts, name, () => listPlaybooks(artifacts, scopes, { text: name }));
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
export function registerPlaybooksVehicleOperations(registry: VehicleRegistry, deps: PlaybooksVehicleDeps): void {
|
|
@@ -33,6 +33,7 @@ function resolveRuleId(
|
|
|
33
33
|
if (typeof id === "string" && id.length > 0) return id;
|
|
34
34
|
if (typeof name !== "string" || name.length === 0) throw validationError("id or name is required");
|
|
35
35
|
return resolveArtifactIdWidened(
|
|
36
|
+
artifacts,
|
|
36
37
|
name,
|
|
37
38
|
() => listRules(artifacts, scopes, { text: name, projectRoot }),
|
|
38
39
|
projectRoot === undefined ? undefined : () => listRules(artifacts, scopes, { text: name }),
|
|
@@ -47,7 +48,7 @@ function resolveRuleId(
|
|
|
47
48
|
function resolveTaskId(artifacts: ArtifactStore, _projectRoot: string | undefined, id: unknown, name: unknown): string | undefined {
|
|
48
49
|
if (typeof id === "string" && id.length > 0) return id;
|
|
49
50
|
if (typeof name !== "string" || name.length === 0) return undefined;
|
|
50
|
-
return resolveArtifactIdWidened(name, () => artifacts.query({ kind: "task", text: name }));
|
|
51
|
+
return resolveArtifactIdWidened(artifacts, name, () => artifacts.query({ kind: "task", text: name }));
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
export function registerRulesVehicleOperations(registry: VehicleRegistry, artifacts: ArtifactStore, scopes: ArtifactScopeStore): void {
|
|
@@ -68,6 +68,7 @@ export interface TasksVehicleDeps {
|
|
|
68
68
|
* carried, hard-won from real cross-project depend/contain friction.
|
|
69
69
|
*/
|
|
70
70
|
function resolveTaskId(
|
|
71
|
+
artifacts: ArtifactStore,
|
|
71
72
|
tasks: Tasks,
|
|
72
73
|
filter: { projectRoot?: string; scope?: TaskViewMode; rootTaskId?: string },
|
|
73
74
|
id: unknown,
|
|
@@ -77,6 +78,7 @@ function resolveTaskId(
|
|
|
77
78
|
if (typeof name !== "string" || name.length === 0) throw validationError("id or name is required");
|
|
78
79
|
if (!filter.projectRoot) throw validationError("project_root is required when resolving a task by name");
|
|
79
80
|
return resolveArtifactIdWidened(
|
|
81
|
+
artifacts,
|
|
80
82
|
name,
|
|
81
83
|
() => tasks.list({ ...filter, text: name }),
|
|
82
84
|
filter.scope === undefined ? () => tasks.list({ ...filter, scope: "all", text: name }) : undefined,
|
|
@@ -84,13 +86,20 @@ function resolveTaskId(
|
|
|
84
86
|
}
|
|
85
87
|
|
|
86
88
|
/** Resolves root_task_name first and scoped to "project" only, matching the removed tool's own resolution order -- every other name lookup below must see the caller's FINAL scope/root selection, which root_task_id itself feeds into. */
|
|
87
|
-
function resolveRootTaskId(
|
|
89
|
+
function resolveRootTaskId(
|
|
90
|
+
artifacts: ArtifactStore,
|
|
91
|
+
tasks: Tasks,
|
|
92
|
+
projectRoot: string | undefined,
|
|
93
|
+
rootTaskId: unknown,
|
|
94
|
+
rootTaskName: unknown,
|
|
95
|
+
): string | undefined {
|
|
88
96
|
if (typeof rootTaskId === "string" && rootTaskId.length > 0) return rootTaskId;
|
|
89
97
|
if (typeof rootTaskName !== "string" || rootTaskName.length === 0) return undefined;
|
|
90
|
-
return resolveTaskId(tasks, { projectRoot, scope: "project" }, undefined, rootTaskName);
|
|
98
|
+
return resolveTaskId(artifacts, tasks, { projectRoot, scope: "project" }, undefined, rootTaskName);
|
|
91
99
|
}
|
|
92
100
|
|
|
93
101
|
function resolveArrayField(
|
|
102
|
+
artifacts: ArtifactStore,
|
|
94
103
|
tasks: Tasks,
|
|
95
104
|
filter: { projectRoot?: string; scope?: TaskViewMode; rootTaskId?: string },
|
|
96
105
|
ids: unknown,
|
|
@@ -98,7 +107,7 @@ function resolveArrayField(
|
|
|
98
107
|
): string[] | undefined {
|
|
99
108
|
if (Array.isArray(ids)) return ids as string[];
|
|
100
109
|
if (!Array.isArray(names) || names.length === 0) return undefined;
|
|
101
|
-
return names.map((entry) => resolveTaskId(tasks, filter, undefined, String(entry)));
|
|
110
|
+
return names.map((entry) => resolveTaskId(artifacts, tasks, filter, undefined, String(entry)));
|
|
102
111
|
}
|
|
103
112
|
|
|
104
113
|
const readSchemaProps = {
|
|
@@ -192,13 +201,13 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
|
|
|
192
201
|
/** Shared by every action taking a single id/name: resolves root_task_name first, then name -> id against the final scope. */
|
|
193
202
|
const resolveIdAndScope = (input: Record<string, unknown>): Record<string, unknown> => {
|
|
194
203
|
const projectRoot = input.project_root as string | undefined;
|
|
195
|
-
const rootTaskId = resolveRootTaskId(tasks, projectRoot, input.root_task_id, input.root_task_name);
|
|
204
|
+
const rootTaskId = resolveRootTaskId(artifacts, tasks, projectRoot, input.root_task_id, input.root_task_name);
|
|
196
205
|
const scope = input.scope as TaskViewMode | undefined;
|
|
197
206
|
const filter = { projectRoot, scope, rootTaskId };
|
|
198
207
|
return {
|
|
199
208
|
...input,
|
|
200
209
|
...(rootTaskId ? { root_task_id: rootTaskId } : {}),
|
|
201
|
-
id: resolveTaskId(tasks, filter, input.id, input.name),
|
|
210
|
+
id: resolveTaskId(artifacts, tasks, filter, input.id, input.name),
|
|
202
211
|
};
|
|
203
212
|
};
|
|
204
213
|
|
|
@@ -230,9 +239,9 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
|
|
|
230
239
|
typeof input.parent_id === "string" && input.parent_id.length > 0
|
|
231
240
|
? input.parent_id
|
|
232
241
|
: typeof input.parent_name === "string" && input.parent_name.length > 0
|
|
233
|
-
? resolveTaskId(tasks, filter, undefined, input.parent_name)
|
|
242
|
+
? resolveTaskId(artifacts, tasks, filter, undefined, input.parent_name)
|
|
234
243
|
: undefined;
|
|
235
|
-
const dependsOn = resolveArrayField(tasks, filter, input.depends_on, input.depends_on_names);
|
|
244
|
+
const dependsOn = resolveArrayField(artifacts, tasks, filter, input.depends_on, input.depends_on_names);
|
|
236
245
|
return { ...input, ...(parentId ? { parent_id: parentId } : {}), ...(dependsOn ? { depends_on: dependsOn } : {}) };
|
|
237
246
|
},
|
|
238
247
|
);
|
|
@@ -263,7 +272,7 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
|
|
|
263
272
|
readSchemaProps,
|
|
264
273
|
["project_root"],
|
|
265
274
|
(input) => {
|
|
266
|
-
const rootTaskId = resolveRootTaskId(tasks, input.project_root as string, input.root_task_id, input.root_task_name);
|
|
275
|
+
const rootTaskId = resolveRootTaskId(artifacts, tasks, input.project_root as string, input.root_task_id, input.root_task_name);
|
|
267
276
|
return { ...input, ...(rootTaskId ? { root_task_id: rootTaskId } : {}) };
|
|
268
277
|
},
|
|
269
278
|
);
|
|
@@ -275,7 +284,7 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
|
|
|
275
284
|
readSchemaProps,
|
|
276
285
|
["project_root"],
|
|
277
286
|
(input) => {
|
|
278
|
-
const rootTaskId = resolveRootTaskId(tasks, input.project_root as string, input.root_task_id, input.root_task_name);
|
|
287
|
+
const rootTaskId = resolveRootTaskId(artifacts, tasks, input.project_root as string, input.root_task_id, input.root_task_name);
|
|
279
288
|
return { ...input, ...(rootTaskId ? { root_task_id: rootTaskId } : {}) };
|
|
280
289
|
},
|
|
281
290
|
);
|
|
@@ -287,7 +296,7 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
|
|
|
287
296
|
readSchemaProps,
|
|
288
297
|
["project_root"],
|
|
289
298
|
(input) => {
|
|
290
|
-
const rootTaskId = resolveRootTaskId(tasks, input.project_root as string, input.root_task_id, input.root_task_name);
|
|
299
|
+
const rootTaskId = resolveRootTaskId(artifacts, tasks, input.project_root as string, input.root_task_id, input.root_task_name);
|
|
291
300
|
return { ...input, ...(rootTaskId ? { root_task_id: rootTaskId } : {}) };
|
|
292
301
|
},
|
|
293
302
|
(input) => {
|
|
@@ -352,7 +361,7 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
|
|
|
352
361
|
},
|
|
353
362
|
["project_root", "scope"],
|
|
354
363
|
(input) => {
|
|
355
|
-
const rootTaskId = resolveRootTaskId(tasks, input.project_root as string, input.root_task_id, input.root_task_name);
|
|
364
|
+
const rootTaskId = resolveRootTaskId(artifacts, tasks, input.project_root as string, input.root_task_id, input.root_task_name);
|
|
356
365
|
return { ...input, ...(rootTaskId ? { root_task_id: rootTaskId } : {}) };
|
|
357
366
|
},
|
|
358
367
|
);
|
|
@@ -363,7 +372,10 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
|
|
|
363
372
|
"local-write",
|
|
364
373
|
{ id: stringProp, name: stringProp, project_root: stringProp, session_id: stringProp },
|
|
365
374
|
["project_root"],
|
|
366
|
-
(input) => ({
|
|
375
|
+
(input) => ({
|
|
376
|
+
...input,
|
|
377
|
+
id: resolveTaskId(artifacts, tasks, { projectRoot: input.project_root as string | undefined }, input.id, input.name),
|
|
378
|
+
}),
|
|
367
379
|
);
|
|
368
380
|
|
|
369
381
|
define(
|
|
@@ -415,7 +427,10 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
|
|
|
415
427
|
"Sets the active Task Focus (singular per scope) to this Task. Multiple sessions can focus the same task while only one holds its lease.",
|
|
416
428
|
{ id: stringProp, name: stringProp, project_root: stringProp },
|
|
417
429
|
[],
|
|
418
|
-
(input) => ({
|
|
430
|
+
(input) => ({
|
|
431
|
+
...input,
|
|
432
|
+
id: resolveTaskId(artifacts, tasks, { projectRoot: input.project_root as string | undefined }, input.id, input.name),
|
|
433
|
+
}),
|
|
419
434
|
);
|
|
420
435
|
focusOperation("pause", "Pauses the active Task Focus without clearing it.", { reason: stringProp }, [], (input) => input);
|
|
421
436
|
focusOperation("unpause", "Resumes a paused Task Focus.", {}, [], (input) => input);
|
|
@@ -588,8 +603,8 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
|
|
|
588
603
|
const filter = { projectRoot: input.project_root as string | undefined, scope: input.scope as TaskViewMode | undefined };
|
|
589
604
|
return {
|
|
590
605
|
...input,
|
|
591
|
-
id: resolveTaskId(tasks, filter, input.id, input.name),
|
|
592
|
-
dependency_id: resolveTaskId(tasks, filter, input.dependency_id, input.dependency_name),
|
|
606
|
+
id: resolveTaskId(artifacts, tasks, filter, input.id, input.name),
|
|
607
|
+
dependency_id: resolveTaskId(artifacts, tasks, filter, input.dependency_id, input.dependency_name),
|
|
593
608
|
};
|
|
594
609
|
},
|
|
595
610
|
);
|
|
@@ -612,8 +627,8 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
|
|
|
612
627
|
const filter = { projectRoot: input.project_root as string | undefined, scope: input.scope as TaskViewMode | undefined };
|
|
613
628
|
return {
|
|
614
629
|
...input,
|
|
615
|
-
id: resolveTaskId(tasks, filter, input.id, input.name),
|
|
616
|
-
dependency_id: resolveTaskId(tasks, filter, input.dependency_id, input.dependency_name),
|
|
630
|
+
id: resolveTaskId(artifacts, tasks, filter, input.id, input.name),
|
|
631
|
+
dependency_id: resolveTaskId(artifacts, tasks, filter, input.dependency_id, input.dependency_name),
|
|
617
632
|
};
|
|
618
633
|
},
|
|
619
634
|
);
|
|
@@ -636,8 +651,8 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
|
|
|
636
651
|
const filter = { projectRoot: input.project_root as string | undefined, scope: input.scope as TaskViewMode | undefined };
|
|
637
652
|
return {
|
|
638
653
|
...input,
|
|
639
|
-
parent_id: resolveTaskId(tasks, filter, input.parent_id, input.parent_name),
|
|
640
|
-
child_id: resolveTaskId(tasks, filter, input.child_id, input.child_name),
|
|
654
|
+
parent_id: resolveTaskId(artifacts, tasks, filter, input.parent_id, input.parent_name),
|
|
655
|
+
child_id: resolveTaskId(artifacts, tasks, filter, input.child_id, input.child_name),
|
|
641
656
|
};
|
|
642
657
|
},
|
|
643
658
|
);
|
|
@@ -660,8 +675,8 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
|
|
|
660
675
|
const filter = { projectRoot: input.project_root as string | undefined, scope: input.scope as TaskViewMode | undefined };
|
|
661
676
|
return {
|
|
662
677
|
...input,
|
|
663
|
-
parent_id: resolveTaskId(tasks, filter, input.parent_id, input.parent_name),
|
|
664
|
-
child_id: resolveTaskId(tasks, filter, input.child_id, input.child_name),
|
|
678
|
+
parent_id: resolveTaskId(artifacts, tasks, filter, input.parent_id, input.parent_name),
|
|
679
|
+
child_id: resolveTaskId(artifacts, tasks, filter, input.child_id, input.child_name),
|
|
665
680
|
};
|
|
666
681
|
},
|
|
667
682
|
);
|
|
@@ -682,7 +697,7 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
|
|
|
682
697
|
[],
|
|
683
698
|
(input) => ({
|
|
684
699
|
...input,
|
|
685
|
-
id: resolveTaskId(tasks, { projectRoot: input.project_root as string | undefined }, input.id, input.name),
|
|
700
|
+
id: resolveTaskId(artifacts, tasks, { projectRoot: input.project_root as string | undefined }, input.id, input.name),
|
|
686
701
|
owner: input.owner ?? input.session_id,
|
|
687
702
|
}),
|
|
688
703
|
);
|
|
@@ -700,7 +715,10 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
|
|
|
700
715
|
session_id: stringProp,
|
|
701
716
|
},
|
|
702
717
|
["owner", "token"],
|
|
703
|
-
(input) => ({
|
|
718
|
+
(input) => ({
|
|
719
|
+
...input,
|
|
720
|
+
id: resolveTaskId(artifacts, tasks, { projectRoot: input.project_root as string | undefined }, input.id, input.name),
|
|
721
|
+
}),
|
|
704
722
|
);
|
|
705
723
|
define(
|
|
706
724
|
"release_lease",
|
|
@@ -708,7 +726,10 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
|
|
|
708
726
|
"local-write",
|
|
709
727
|
{ id: stringProp, name: stringProp, owner: stringProp, token: stringProp, project_root: stringProp, session_id: stringProp },
|
|
710
728
|
["owner", "token"],
|
|
711
|
-
(input) => ({
|
|
729
|
+
(input) => ({
|
|
730
|
+
...input,
|
|
731
|
+
id: resolveTaskId(artifacts, tasks, { projectRoot: input.project_root as string | undefined }, input.id, input.name),
|
|
732
|
+
}),
|
|
712
733
|
);
|
|
713
734
|
define(
|
|
714
735
|
"lease",
|
|
@@ -716,7 +737,10 @@ export function registerTasksVehicleOperations(registry: VehicleRegistry, deps:
|
|
|
716
737
|
"read",
|
|
717
738
|
{ id: stringProp, name: stringProp, project_root: stringProp },
|
|
718
739
|
[],
|
|
719
|
-
(input) => ({
|
|
740
|
+
(input) => ({
|
|
741
|
+
...input,
|
|
742
|
+
id: resolveTaskId(artifacts, tasks, { projectRoot: input.project_root as string | undefined }, input.id, input.name),
|
|
743
|
+
}),
|
|
720
744
|
);
|
|
721
745
|
|
|
722
746
|
define(
|