@indigoai-us/hq-cli 5.345.88 → 5.345.90
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/CHANGELOG.md +8 -0
- package/assets/bot-workers/setup/skills/your-business.md +46 -12
- package/assets/bot-workers/setup/worker.yaml +152 -9
- package/assets/scaffold/core/scripts/hq-delegate-bundle.sh +107 -21
- package/assets/scaffold/core/scripts/hq-delegate-grant.sh +10 -3
- package/assets/scaffold/core/scripts/hq-delegate-publish.sh +36 -5
- package/assets/scaffold/core/scripts/hq-delegate-send.sh +18 -3
- package/assets/scaffold/core/scripts/hq-delegate-transfer.sh +74 -26
- package/assets/scaffold/core/scripts/hq-delegate-verify.sh +10 -1
- package/assets/scaffold/core/scripts/hq-job-probe.sh +10 -3
- package/assets/scaffold/core/scripts/jobs-validate.sh +10 -3
- package/assets/scaffold/core/scripts/lib/delegate-brainstorm.sh +120 -0
- package/dist/commands/core-delegate-brainstorm.d.ts +9 -0
- package/dist/commands/core-delegate-brainstorm.js +161 -0
- package/dist/commands/core-delegate-w1b.js +134 -40
- package/dist/commands/core-delegate.d.ts +2 -0
- package/dist/commands/core-delegate.js +56 -29
- package/dist/commands/core.js +1 -0
- package/dist/lib/bot/dm-history.d.ts +37 -0
- package/dist/lib/bot/dm-history.js +126 -0
- package/dist/lib/bot/paths.d.ts +6 -0
- package/dist/lib/bot/paths.js +15 -0
- package/dist/lib/bot/prompt.js +1 -0
- package/dist/lib/bot/run.d.ts +5 -3
- package/dist/lib/bot/run.js +198 -101
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Native implementations of the second Wave 1 delegate command family. */
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
2
|
+
import { createHash, randomInt } from "node:crypto";
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import * as os from "node:os";
|
|
5
5
|
import * as path from "node:path";
|
|
@@ -13,7 +13,10 @@ import { ACL_SOURCE_DISPLAY_LABELS } from "../utils/files-browse-acl.js";
|
|
|
13
13
|
import { runBundledScript } from "../utils/run-bundled-script.js";
|
|
14
14
|
import { resolveWindowsBash } from "../utils/windows-bash.js";
|
|
15
15
|
import { createShellCompatibleJqInvoker } from "../utils/jq-windows-compat.js";
|
|
16
|
+
import { brainstormDossier, brainstormFrontmatterGet, brainstormSection, brainstormSummary, projectStage } from "./core-delegate-brainstorm.js";
|
|
16
17
|
const FLAG = "core-native-delegate";
|
|
18
|
+
const MKTEMP_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
19
|
+
const MKTEMP_SUFFIX_LENGTH = 10;
|
|
17
20
|
const ASSETS = {
|
|
18
21
|
bundle: "core/scripts/hq-delegate-bundle.sh",
|
|
19
22
|
grant: "core/scripts/hq-delegate-grant.sh",
|
|
@@ -32,7 +35,7 @@ const USAGE = {
|
|
|
32
35
|
bundle: "hq-delegate-bundle.sh — freeze a project into a portable delegation bundle.\n\nUsage:\n core/scripts/hq-delegate-bundle.sh build \\\n --company <slug> --project <name> --to <principal> \\\n [--to-kind person|agent] [--to-name <display name>] \\\n [--mode transfer|share] [--dry-run]\n\n--dry-run prints the manifest JSON to stdout and writes nothing.\n\nWrites workspace/delegations/<delegationId>/{manifest.json,BRIEF.md} and\n",
|
|
33
36
|
grant: "hq-delegate-grant.sh — materialize a delegation's dossier in the vault and\nwrite the ACL grants its manifest declares, verifying each one landed.\n\nUsage:\n core/scripts/hq-delegate-grant.sh --manifest <path> [--yes]\n\nWithout --yes: prints the full grant plan (prefix, principal, permission —\nthe write grant is a privilege escalation and needs explicit confirmation)\nand exits 2 without mutating anything. The caller (the /delegate skill)\nconfirms with the user, then re-invokes with --yes.\n\nWith --yes:\n 1. hq sync push the project directory so the vault prefix exists\n 2. hq files share each manifest vaultPrefix to the recipient\n 3. read each grant back with hq files acl and match grantee + permission\n 4. advance manifest status building -> granted, stamping verifiedAt\n\nDirect ACL grants ONLY — this helper never mints a share-session URL\n(policy hq-delegate-never-inlines-secrets-or-share-urls).\n\nIdempotent: a prefix whose grant already exists is reported, not an error.\nA failed read-back exits non-zero and leaves manifest status unchanged.\n\n",
|
|
34
37
|
send: "hq-delegate-send.sh — generate the self-sufficient pickup prompt for a\ndelegation and deliver it by DM. The prompt IS the handoff: pasted into a\nfresh agent session on a clean HQ install, it pulls every file on demand\n(hq files get per prefix — no /hq-sync run) and reaches the first\nnext-step with no follow-up question to the delegator.\n\nUsage:\n core/scripts/hq-delegate-send.sh --manifest <path> [--send]\n [--headline <text>] [--note <text>]\n\nWithout --send: (re)generates PICKUP-PROMPT.md next to the manifest and\nprints its path — inspectable, idempotent, sends nothing.\nWith --send: requires manifest status \"verified\" (the US-008 probe must\nhave passed), then delivers ONE `hq dm` with --prompt-file/--details-file\nand advances status to \"sent\", recording the DM eventId.\n\nThe caller (the /delegate skill) humanizes --headline / --note per\ncore/knowledge/public/hq-core/humanize-before-send.md (channel dm, light)\nBEFORE invoking; this helper never rewrites recipients, flags, or the\ngenerated command lines.\n\nFail-closed: the generated prompt is scanned for secret-shaped content and\nshare-session URLs; a match aborts before anything is written or sent\n(policy hq-delegate-never-inlines-secrets-or-share-urls).\n\n",
|
|
35
|
-
verify: "hq-delegate-verify.sh — prove a delegation can actually be picked up\nbefore anyone is told it is theirs, or print the full plan without\ntouching anything.\n\nUsage (probe mode — after grants, before the DM):\n core/scripts/hq-delegate-verify.sh --manifest <path>\n\nProbes every manifest vaultPrefix with `hq files browse` and requires it\nreachable AND non-empty. All pass -> manifest advances granted -> verified\n(send is gated on this). Any failure -> non-zero, the failing prefix and\nlikely cause named, status left at its last successful value so a re-run\nresumes without re-granting.\n\nUsage (dry-run mode — full plan, zero mutation):\n core/scripts/hq-delegate-verify.sh --dry-run \\\n --company <slug> --project <name> --to <principal> [--mode transfer|share]\n\nPrints recipient, mode, every prefix with its permission, the secret\nnames an .env.schema would grant, repo + branch, and the DM headline that\nwould be sent. Exits 0. Creates nothing under workspace/delegations/ and\ninvokes no mutating command.\n\
|
|
38
|
+
verify: "hq-delegate-verify.sh — prove a delegation can actually be picked up\nbefore anyone is told it is theirs, or print the full plan without\ntouching anything.\n\nUsage (probe mode — after grants, before the DM):\n core/scripts/hq-delegate-verify.sh --manifest <path>\n\nProbes every manifest vaultPrefix with `hq files browse` and requires it\nreachable AND non-empty. All pass -> manifest advances granted -> verified\n(send is gated on this). Any failure -> non-zero, the failing prefix and\nlikely cause named, status left at its last successful value so a re-run\nresumes without re-granting.\n\nUsage (dry-run mode — full plan, zero mutation):\n core/scripts/hq-delegate-verify.sh --dry-run \\\n --company <slug> --project <name> --to <principal> [--mode transfer|share]\n\nPrints recipient, mode, project stage (prd, or brainstorm with the dossier\nfiles and the /plan next step), every prefix with its permission, the secret\nnames an .env.schema would grant, repo + branch, and the DM headline that\nwould be sent. Exits 0. Creates nothing under workspace/delegations/ and\ninvokes no mutating command.\n\n",
|
|
36
39
|
publish: "usage: --manifest <path>",
|
|
37
40
|
};
|
|
38
41
|
function out(value) { process.stdout.write(value); }
|
|
@@ -56,6 +59,9 @@ function currentTime() {
|
|
|
56
59
|
const date = value === undefined ? new Date() : new Date(value);
|
|
57
60
|
return Number.isNaN(date.getTime()) ? "Invalid Date" : date.toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
58
61
|
}
|
|
62
|
+
function mktempCompatibleSuffix() {
|
|
63
|
+
return Array.from({ length: MKTEMP_SUFFIX_LENGTH }, () => MKTEMP_ALPHABET[randomInt(MKTEMP_ALPHABET.length)]).join("");
|
|
64
|
+
}
|
|
59
65
|
function jsonFile(file) {
|
|
60
66
|
try {
|
|
61
67
|
const value = readJsonFile(file);
|
|
@@ -254,17 +260,23 @@ function runBundle(args, context, onMalformedWhoami) {
|
|
|
254
260
|
const prdPath = path.join(projectDir, "prd.json");
|
|
255
261
|
if (!fs.existsSync(projectDir) || !fs.statSync(projectDir).isDirectory())
|
|
256
262
|
return fail("bundle", `project directory not found: companies/${company}/projects/${project}`);
|
|
257
|
-
|
|
258
|
-
|
|
263
|
+
const stage = projectStage(projectDir);
|
|
264
|
+
if (stage === "none")
|
|
265
|
+
return fail("bundle", `project has neither prd.json nor brainstorm.md: companies/${company}/projects/${project} (run /brainstorm or /plan first)`);
|
|
259
266
|
let prd;
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
267
|
+
if (stage === "prd") {
|
|
268
|
+
try {
|
|
269
|
+
prd = readJsonFile(prdPath);
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
return fail("bundle", `prd.json is not valid JSON: ${prdPath}`);
|
|
273
|
+
}
|
|
274
|
+
if (prd === null || prd === false)
|
|
275
|
+
return fail("bundle", `prd.json is not valid JSON: ${prdPath}`);
|
|
265
276
|
}
|
|
266
|
-
|
|
267
|
-
|
|
277
|
+
const projectRelative = `companies/${company}/projects/${project}`;
|
|
278
|
+
const brainstormPath = path.join(projectDir, "brainstorm.md");
|
|
279
|
+
const brainstormRelative = `${projectRelative}/brainstorm.md`;
|
|
268
280
|
const who = subprocess("hq", ["whoami", "--json"], context.cwd, { stdout: "capture" });
|
|
269
281
|
if (who.code !== 0)
|
|
270
282
|
return fail("bundle", "could not read sender identity; update hq-cli if --json is unsupported");
|
|
@@ -283,7 +295,8 @@ function runBundle(args, context, onMalformedWhoami) {
|
|
|
283
295
|
return fail("bundle", "sender identity is missing or expired; run hq login");
|
|
284
296
|
const fromEmail = text(identityValue.email) || null;
|
|
285
297
|
const fromUid = text(identityValue.personUid) || text(identityValue.agentUid) || null;
|
|
286
|
-
const knowledge = list(getPath(prd, "metadata.knowledge", []))
|
|
298
|
+
const knowledge = (stage === "prd" ? list(getPath(prd, "metadata.knowledge", [])) : [])
|
|
299
|
+
.filter((entry) => typeof entry === "string").map(String);
|
|
287
300
|
const readPrefixes = [...new Set(knowledge.filter((entry) => entry.startsWith(`companies/${company}/`)).map((entry) => {
|
|
288
301
|
const relative = entry.slice(`companies/${company}/`.length);
|
|
289
302
|
if (relative.endsWith("/"))
|
|
@@ -297,7 +310,7 @@ function runBundle(args, context, onMalformedWhoami) {
|
|
|
297
310
|
].sort((a, b) => a.prefix.localeCompare(b.prefix));
|
|
298
311
|
if (vaultPrefixes.some(({ prefix }) => !prefix.endsWith("/") || prefix.startsWith("companies/")))
|
|
299
312
|
return fail("bundle", "internal error: derived a non-folder or company-anchored prefix");
|
|
300
|
-
const repoPath = text(getPath(prd, "metadata.repoPath"));
|
|
313
|
+
const repoPath = stage === "prd" ? text(getPath(prd, "metadata.repoPath")) : "";
|
|
301
314
|
const repo = repoPath ? {
|
|
302
315
|
path: repoPath,
|
|
303
316
|
remote: null,
|
|
@@ -312,7 +325,9 @@ function runBundle(args, context, onMalformedWhoami) {
|
|
|
312
325
|
const board = jsonFile(boardFile);
|
|
313
326
|
if (board) {
|
|
314
327
|
const projects = list(board.projects);
|
|
315
|
-
const
|
|
328
|
+
const lookupPath = stage === "prd" ? `${projectRelative}/prd.json` : brainstormRelative;
|
|
329
|
+
const lookupKey = stage === "prd" ? "prd_path" : "brainstorm_path";
|
|
330
|
+
const boardEntry = projects.find((entry) => isRecord(entry) && entry[lookupKey] === lookupPath);
|
|
316
331
|
if (isRecord(boardEntry) && jqTrue(boardEntry.id))
|
|
317
332
|
boardId = boardEntry.id;
|
|
318
333
|
}
|
|
@@ -323,6 +338,9 @@ function runBundle(args, context, onMalformedWhoami) {
|
|
|
323
338
|
const idTime = createdAt.replace(/[-:TZ]/g, "").slice(0, 8) + "-" + createdAt.slice(11, 19).replaceAll(":", "");
|
|
324
339
|
const delegationId = `dlg-${idTime}-${project}`;
|
|
325
340
|
const policies = knowledge.filter((entry) => entry.startsWith(`companies/${company}/policies/`));
|
|
341
|
+
const projectManifest = stage === "prd"
|
|
342
|
+
? { name: project, stage, prdPath: `${projectRelative}/prd.json`, sourcePath: `${projectRelative}/prd.json`, boardId }
|
|
343
|
+
: { name: project, stage, prdPath: null, sourcePath: brainstormRelative, boardId, dossier: brainstormDossier(root, projectRelative) };
|
|
326
344
|
const manifest = {
|
|
327
345
|
schemaVersion: 1,
|
|
328
346
|
delegationId,
|
|
@@ -331,7 +349,7 @@ function runBundle(args, context, onMalformedWhoami) {
|
|
|
331
349
|
from: { email: fromEmail, personUid: fromUid },
|
|
332
350
|
to: { kind: toKind, principal: recipient, displayName: toName || null },
|
|
333
351
|
company,
|
|
334
|
-
project:
|
|
352
|
+
project: projectManifest,
|
|
335
353
|
vaultPrefixes: vaultPrefixes.filter((entry, index, all) => all.findIndex((candidate) => candidate.prefix === entry.prefix) === index),
|
|
336
354
|
repo,
|
|
337
355
|
secrets: [],
|
|
@@ -340,6 +358,16 @@ function runBundle(args, context, onMalformedWhoami) {
|
|
|
340
358
|
checksums,
|
|
341
359
|
status: "building",
|
|
342
360
|
};
|
|
361
|
+
if (stage === "brainstorm") {
|
|
362
|
+
const summary = brainstormSummary(brainstormPath) || "No summary recorded in brainstorm.md.";
|
|
363
|
+
const recommendation = brainstormSection(brainstormPath, "Recommendation");
|
|
364
|
+
const unknowns = brainstormSection(brainstormPath, "What We Don't Know");
|
|
365
|
+
const dossierList = projectManifest.dossier
|
|
366
|
+
.map((entry) => `- \`${entry}\``)
|
|
367
|
+
.join("\n");
|
|
368
|
+
const brief = `# Delegation brief — ${project}\n\nThis brief describes the **${project}** project in the **${company}** company. Completed handoff steps are recorded in the manifest. Pull the files and acknowledge receipt; report any access error to the sender.\n\n## What this project is\n\n${summary}\n\n## Where things stand\n\nThis project is at the brainstorm stage. There is no PRD yet and no stories have been written; the source of truth is \`${projectRelative}/brainstorm.md\`, with research notes and the latest session journal alongside it.\n\n${recommendation ? `## Recommendation so far\n\n${recommendation}\n\n` : ""}${unknowns ? `## Open questions\n\n${unknowns}\n\n` : ""}## Recommended next step\n\nRun \`/plan ${project}\` to turn the brainstorm into a PRD with stories. Read brainstorm.md and the research notes first; the recommendation and open questions above come from them.\n\n## Where everything lives\n\nThe dossier for this handoff:\n\n${dossierList}\n\nThe pickup prompt that accompanied this brief pulls every file you need on demand — you do not need to run a full sync.\n`;
|
|
369
|
+
return writeBundle(context, { manifest, brief, dryRun, project });
|
|
370
|
+
}
|
|
343
371
|
const description = text(getPath(prd, "description"), "No description recorded.");
|
|
344
372
|
const goal = text(getPath(prd, "metadata.goal"));
|
|
345
373
|
const stories = list(getPath(prd, "userStories", []));
|
|
@@ -365,27 +393,54 @@ function runBundle(args, context, onMalformedWhoami) {
|
|
|
365
393
|
if (traps)
|
|
366
394
|
brief += `\n## Known traps and conventions\n\n${traps}\n`;
|
|
367
395
|
brief += `\n## Where everything lives\n\nThe full PRD (every story, acceptance criteria, and decision history) is at \`companies/${company}/projects/${project}/prd.json\`. The pickup prompt that accompanied this brief pulls every file you need on demand — you do not need to run a full sync.\n`;
|
|
368
|
-
|
|
396
|
+
return writeBundle(context, { manifest, brief, dryRun, project });
|
|
397
|
+
}
|
|
398
|
+
function writeBundle(context, input) {
|
|
399
|
+
const root = context.root ?? context.cwd;
|
|
400
|
+
const { manifest, brief, dryRun, project } = input;
|
|
401
|
+
const delegationId = jqRawPath(manifest, "delegationId");
|
|
402
|
+
const destinationParent = path.join(root, "workspace", "delegations");
|
|
403
|
+
// The shell uses `mktemp -d`, which honors TMPDIR on every supported shell.
|
|
404
|
+
const stageBase = process.env.TMPDIR || os.tmpdir();
|
|
405
|
+
const stage = path.join(stageBase, `tmp.${mktempCompatibleSuffix()}`);
|
|
406
|
+
fs.mkdirSync(stage, { mode: 0o700 });
|
|
407
|
+
const bundleFileSystem = context.bundleFileSystem ?? fs;
|
|
369
408
|
const manifestFile = path.join(stage, "manifest.json");
|
|
370
409
|
const briefFile = path.join(stage, "BRIEF.md");
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
410
|
+
try {
|
|
411
|
+
fs.writeFileSync(manifestFile, jsonText(manifest));
|
|
412
|
+
fs.writeFileSync(briefFile, brief);
|
|
413
|
+
if (!secretScan("bundle", [manifestFile, briefFile])) {
|
|
414
|
+
return fail("bundle", "generated bundle matched a secret-detection pattern — nothing written (see stderr above)");
|
|
415
|
+
}
|
|
416
|
+
if (dryRun) {
|
|
417
|
+
out(fs.readFileSync(manifestFile, "utf8"));
|
|
418
|
+
err("hq-delegate-bundle: dry run — nothing written\n");
|
|
419
|
+
return 0;
|
|
420
|
+
}
|
|
421
|
+
const dest = path.join(destinationParent, delegationId);
|
|
422
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
423
|
+
try {
|
|
424
|
+
bundleFileSystem.renameSync(stage, dest);
|
|
425
|
+
}
|
|
426
|
+
catch (error) {
|
|
427
|
+
const code = typeof error === "object" && error !== null && "code" in error
|
|
428
|
+
? error.code
|
|
429
|
+
: undefined;
|
|
430
|
+
if (code !== "EXDEV" && code !== "EPERM")
|
|
431
|
+
throw error;
|
|
432
|
+
// `mv` crosses volumes by recursively copying a directory before
|
|
433
|
+
// removing the source. copyFileSync cannot handle this directory.
|
|
434
|
+
bundleFileSystem.cpSync(stage, dest, { recursive: true });
|
|
435
|
+
bundleFileSystem.rmSync(stage, { recursive: true, force: true });
|
|
436
|
+
}
|
|
437
|
+
out(`${delegationId}\n`);
|
|
438
|
+
err(`hq-delegate-bundle: wrote workspace/delegations/${delegationId}/{manifest.json,BRIEF.md}\n`);
|
|
381
439
|
return 0;
|
|
382
440
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
out(`${delegationId}\n`);
|
|
387
|
-
err(`hq-delegate-bundle: wrote workspace/delegations/${delegationId}/{manifest.json,BRIEF.md}\n`);
|
|
388
|
-
return 0;
|
|
441
|
+
finally {
|
|
442
|
+
bundleFileSystem.rmSync(stage, { recursive: true, force: true });
|
|
443
|
+
}
|
|
389
444
|
}
|
|
390
445
|
function runHq(args, context, stdout = "capture") {
|
|
391
446
|
return subprocess("hq", args, context.cwd, { stdout });
|
|
@@ -737,22 +792,41 @@ function runPublish(args, context) {
|
|
|
737
792
|
const bundleDir = path.dirname(path.resolve(manifestPath));
|
|
738
793
|
const brief = path.join(bundleDir, "BRIEF.md");
|
|
739
794
|
const prd = path.join(projectDir, "prd.json");
|
|
795
|
+
const brainstorm = path.join(projectDir, "brainstorm.md");
|
|
796
|
+
const stageKind = jqRawPath(manifest, "project.stage", "prd");
|
|
740
797
|
if (!fs.existsSync(brief) || !fs.statSync(brief).isFile())
|
|
741
798
|
return fail("publish", "BRIEF.md missing");
|
|
742
|
-
if (!fs.existsSync(prd) || !fs.statSync(prd).isFile())
|
|
799
|
+
if (stageKind === "prd" && (!fs.existsSync(prd) || !fs.statSync(prd).isFile()))
|
|
743
800
|
return fail("publish", "PRD missing");
|
|
801
|
+
if (stageKind === "brainstorm" && (!fs.existsSync(brainstorm) || !fs.statSync(brainstorm).isFile()))
|
|
802
|
+
return fail("publish", "brainstorm.md missing");
|
|
803
|
+
if (stageKind !== "prd" && stageKind !== "brainstorm")
|
|
804
|
+
return fail("publish", `unknown project stage '${stageKind}'`);
|
|
744
805
|
if (jqRawPath(manifest, "mode") === "transfer") {
|
|
745
806
|
const expectedOwner = jqRawPath(manifest, "to.principal");
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
807
|
+
if (stageKind === "prd") {
|
|
808
|
+
const prdValue = jsonFile(prd);
|
|
809
|
+
if (!prdValue || jqRawPath(prdValue, "metadata.owner") !== expectedOwner)
|
|
810
|
+
return fail("publish", "local PRD owner does not match recipient");
|
|
811
|
+
}
|
|
812
|
+
else if (brainstormFrontmatterGet(brainstorm, "owner") !== expectedOwner)
|
|
813
|
+
return fail("publish", "local brainstorm.md owner does not match recipient");
|
|
749
814
|
}
|
|
750
815
|
stage = fs.mkdtempSync(path.join(os.tmpdir(), "tmp-"));
|
|
751
816
|
const stagedBrief = path.join(stage, "BRIEF.md");
|
|
752
817
|
fs.copyFileSync(brief, stagedBrief);
|
|
753
818
|
const hashFile = path.join(stage, "hashes");
|
|
754
819
|
const hashed = {};
|
|
755
|
-
|
|
820
|
+
const snapshotFiles = stageKind === "prd"
|
|
821
|
+
? [prd, path.join(projectDir, "README.md"), path.join(projectDir, "journal", "delegations.md")]
|
|
822
|
+
: [...list(getPath(manifest, "project.dossier", [])).map((entry) => path.join(root, text(entry))), path.join(projectDir, "journal", "delegations.md")];
|
|
823
|
+
if (stageKind === "brainstorm") {
|
|
824
|
+
for (const file of snapshotFiles.slice(0, -1)) {
|
|
825
|
+
if (!fs.existsSync(file) || !fs.statSync(file).isFile())
|
|
826
|
+
return fail("publish", `dossier file missing: ${path.relative(root, file).split(path.sep).join("/")}`);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
for (const file of snapshotFiles) {
|
|
756
830
|
if (fs.existsSync(file) && fs.statSync(file).isFile()) {
|
|
757
831
|
const key = path.relative(root, file).split(path.sep).join("/");
|
|
758
832
|
hashed[key] = checksum(file);
|
|
@@ -891,10 +965,20 @@ function runSend(args, context) {
|
|
|
891
965
|
const prdRelative = jqRawPath(manifest, "project.prdPath");
|
|
892
966
|
const prdAbs = path.join(root, prdRelative);
|
|
893
967
|
const prd = fs.existsSync(prdAbs) ? jsonFile(prdAbs) : undefined;
|
|
968
|
+
const stageKind = jqRawPath(manifest, "project.stage", "prd");
|
|
969
|
+
const brainstormAbs = path.join(root, "companies", company, "projects", project, "brainstorm.md");
|
|
894
970
|
let goal = "";
|
|
895
971
|
let stateLine = "";
|
|
896
972
|
let nextSteps = "";
|
|
897
|
-
|
|
973
|
+
let dossierLine = `Start with the project dossier: \`companies/${company}/projects/${project}/\` now contains the PRD (\`prd.json\`), a full brief (\`README.md\` / the DM's details pane), and the delegation journal.`;
|
|
974
|
+
if (stageKind === "brainstorm") {
|
|
975
|
+
if (fs.existsSync(brainstormAbs))
|
|
976
|
+
goal = brainstormSummary(brainstormAbs);
|
|
977
|
+
stateLine = `This project is at the brainstorm stage: no PRD yet, no stories. The source of truth is \`companies/${company}/projects/${project}/brainstorm.md\`.`;
|
|
978
|
+
nextSteps = `1. **Read the brainstorm** — \`brainstorm.md\`, the notes under \`research/\`, and the latest journal entry under \`journal/\`.\n2. **Turn it into a PRD** — run \`/plan ${project}\` to write \`prd.json\` with stories.\n3. **Start the first story** — once the PRD exists, \`/run-project ${project}\` or \`/execute-task\` picks it up.`;
|
|
979
|
+
dossierLine = `Start with the project dossier: \`companies/${company}/projects/${project}/\` contains \`brainstorm.md\`, the \`research/\` notes, the latest journal entry, a full brief (the DM's details pane), and the delegation journal. There is no \`prd.json\` yet.`;
|
|
980
|
+
}
|
|
981
|
+
else if (prd) {
|
|
898
982
|
goal = text(jqFallback(getPath(prd, "metadata.goal"), getPath(prd, "description", "")));
|
|
899
983
|
const stories = list(getPath(prd, "userStories", []));
|
|
900
984
|
const complete = stories.filter((entry) => isRecord(entry) && entry.passes === true).length;
|
|
@@ -935,7 +1019,7 @@ function runSend(args, context) {
|
|
|
935
1019
|
body += `## The goal\n\n${goal}\n\n`;
|
|
936
1020
|
if (stateLine)
|
|
937
1021
|
body += `## Where things stand\n\n${stateLine}\n\n`;
|
|
938
|
-
body += `## Pull the files (on demand — no sync)\n\nRun these from your HQ root; each materializes exactly that path locally:\n\n\`\`\`bash\n${getLines}\n\`\`\`\n\
|
|
1022
|
+
body += `## Pull the files (on demand — no sync)\n\nRun these from your HQ root; each materializes exactly that path locally:\n\n\`\`\`bash\n${getLines}\n\`\`\`\n\n${dossierLine}\n\n`;
|
|
939
1023
|
if (repoSection)
|
|
940
1024
|
body += `${repoSection}\n\n`;
|
|
941
1025
|
if (secretsSection)
|
|
@@ -1090,7 +1174,17 @@ function runVerify(args, context) {
|
|
|
1090
1174
|
secretNames = names.length ? `${names.join(" ")} ` : "(none declared)";
|
|
1091
1175
|
}
|
|
1092
1176
|
out("Delegation dry run — nothing will be pushed, granted, transferred, or sent.\n\n");
|
|
1093
|
-
|
|
1177
|
+
const stageKind = jqRawPath(manifest, "project.stage", "prd");
|
|
1178
|
+
out(` Recipient: ${recipient}\n Mode: ${mode}\n Company: ${company}\n Project: ${project}\n`);
|
|
1179
|
+
if (stageKind === "brainstorm") {
|
|
1180
|
+
out(` Stage: brainstorm — no PRD yet; source of truth is brainstorm.md; recommended next step is /plan ${project}\n\n`);
|
|
1181
|
+
out(" Dossier files that would be published:\n");
|
|
1182
|
+
for (const file of list(getPath(manifest, "project.dossier", [])))
|
|
1183
|
+
out(` ${text(file)}\n`);
|
|
1184
|
+
}
|
|
1185
|
+
else
|
|
1186
|
+
out(" Stage: prd\n");
|
|
1187
|
+
out("\n");
|
|
1094
1188
|
out(" Vault grants that would be written:\n");
|
|
1095
1189
|
for (const entry of list(getPath(manifest, "vaultPrefixes", [])))
|
|
1096
1190
|
out(` ${jqRawPath(entry, "permission")}\ton ${jqRawPath(entry, "prefix")}\t(${jqRawPath(entry, "reason")})\n`);
|
|
@@ -7,8 +7,10 @@ export type DelegateContext = {
|
|
|
7
7
|
cwd: string;
|
|
8
8
|
root?: string;
|
|
9
9
|
atomicWriteFileSystem?: DelegateAtomicWriteFileSystem;
|
|
10
|
+
bundleFileSystem?: DelegateBundleFileSystem;
|
|
10
11
|
};
|
|
11
12
|
export type DelegateAtomicWriteFileSystem = Pick<typeof fs, "openSync" | "writeFileSync" | "closeSync" | "renameSync" | "copyFileSync" | "unlinkSync" | "rmSync">;
|
|
13
|
+
export type DelegateBundleFileSystem = Pick<typeof fs, "renameSync" | "cpSync" | "rmSync">;
|
|
12
14
|
type CommandResult = {
|
|
13
15
|
code: number;
|
|
14
16
|
stdout?: string;
|
|
@@ -11,6 +11,7 @@ import { buildCmdExeSpawnPlan, isWindowsBatchFile } from "../utils/windows-spawn
|
|
|
11
11
|
import { describeCoreCommand } from "./core-command-catalog.js";
|
|
12
12
|
import { resolveLiveRoot } from "../utils/hq-roots.js";
|
|
13
13
|
import { runBundledScript } from "../utils/run-bundled-script.js";
|
|
14
|
+
import { brainstormFrontmatterSet, brainstormSummary, brainstormTitle } from "./core-delegate-brainstorm.js";
|
|
14
15
|
export const CORE_NATIVE_DELEGATE_FLAG = "core-native-delegate";
|
|
15
16
|
const SCRIPT_ASSETS = {
|
|
16
17
|
resolve: "core/scripts/hq-delegate-resolve.sh",
|
|
@@ -52,7 +53,7 @@ const USAGE_TEXT = {
|
|
|
52
53
|
pickup: "# hq-core: public\n# hq-delegate-pickup.sh — close the delegation loop with a receipt or a FAILED\n# state (2026-09-07).\n#\n# Why: the delegate pipeline ended at \"sent\", which is a send receipt, not a\n# delivery receipt. Seven delegations sat at sent/building with no way to tell\n# whether the recipient could act. This script adds the two missing states:\n#\n# sent --(evidence)--> picked-up recipient demonstrably has the work\n# sent --(silence past window)--> failed nobody picked it up; sender is told why\n#\n# Evidence, checked in order (any one suffices):\n# 1. --ack \"<text>\" an explicit acknowledgement (DM reply, chat) —\n# recorded verbatim with a timestamp\n# 2. repo branch has a commit by the recipient after sentAt\n# (manifest.repo.path + repo.branch, author email == to.principal)\n# 3. --evidence \"<free text>\" any other proof the operator observed\n#\n# Usage:\n# hq-delegate-pickup.sh --manifest <path> --ack \"<text>\" # record receipt\n# hq-delegate-pickup.sh --manifest <path> --evidence \"<text>\" # record receipt\n# hq-delegate-pickup.sh --manifest <path> --check [--window-hours N] # probe repo; fail if silent past window (default 72)\n# hq-delegate-pickup.sh --manifest <path> --status # print state line\n#\n# Exit 0: picked-up (or already). Exit 5: failed (window elapsed, no evidence).\n# Exit 6: still waiting (inside window, no evidence). Exit 2: usage/precondition.\nset -uo pipefail\n",
|
|
53
54
|
repo: "hq-delegate-repo.sh — code/branch handover for a delegation: make sure the\nproject's branch exists on the remote, record uncommitted work as NOT\ntransferred, and verify the recipient's repository access read-only.\n\nUsage:\n core/scripts/hq-delegate-repo.sh --manifest <path> [--yes] [--github-user <login>]\n\nBehavior (driven by the manifest's repo{} block; no-op when repo is null):\n - records remote URL and the branch's headSha into the manifest\n - a branch that exists only locally requires confirmation: without --yes\n the helper prints the plan and exits 2; with --yes it pushes via an\n explicitly anchored `git -C <abs path> push -u origin <branch>`\n - dirty/untracked files are captured into repo.dirtyFiles[] and appended\n to BRIEF.md as work NOT transferred — never silently dropped\n - recipient access is checked read-only via gh when --github-user is\n given; otherwise (or on a gh failure) repo.accessVerified stays false\n and the brief says so plainly — the helper never claims unverified access\n - never touches vaultPrefixes[] and never pushes repo content to the vault\n\nEnv: HQ_ROOT — HQ root override (default: resolved from script location)\n",
|
|
54
55
|
secrets: "hq-delegate-secrets.sh — grant a delegation recipient access to the secrets\na project needs, BY NAME ONLY. No command in this helper ever reads,\nprints, logs, or interpolates a secret value.\n\nUsage:\n core/scripts/hq-delegate-secrets.sh --manifest <path> [--yes] [--no-secrets]\n [--env-schema <path>]\n\nBehavior:\n - derives required secret NAMES from the project's / repo's .env.schema\n (--env-schema overrides discovery); no schema -> records secrets: []\n and says so plainly rather than guessing at credentials\n - without --yes: prints a full-prose confirmation listing every name and\n the principal, then exits 2 granting nothing — this step is a privilege\n handover and gets its own confirmation, separate from the file grants\n - with --yes: hq secrets share <NAME> --with <principal> --permission read\n per name, then records the names (and only the names) in the manifest\n - --no-secrets: skips everything, records {secrets: [], secretsSkipped: true}\n\nConsumption on the recipient's side is via `hq run` / `hq secrets exec` —\ndocumented in the brief; values never appear in any delegation artifact\n(policy hq-delegate-never-inlines-secrets-or-share-urls).\n",
|
|
55
|
-
transfer: "hq-delegate-transfer.sh — make a delegation real everywhere HQ tracks\nownership: board entry, PRD metadata, work mesh, and the project journal.\n\nUsage:\n core/scripts/hq-delegate-transfer.sh --manifest <path>\n\nBehavior (mode \"transfer\"; mode \"share\" skips every mutation):\n 1. companies/<co>/board.json — the project's entry gets owner=<recipient>\n and a bumped updated_at; created (once) if absent\n 2. prd.json metadata gains owner, delegatedFrom, delegatedAt\n 3. work-mesh: `done` event closes the delegator's in-progress thread and\n broadcasts the reassignment; silently skipped when the helper is\n unavailable (local/offline installs) — the transfer still succeeds\n 4. a dated \"Delegated\" stanza is appended to the project journal naming\n the recipient, delegation id, and transferred scope\n\nIdempotent: re-running updates owner/timestamps in place — never a\nduplicate board entry, never a duplicated journal stanza.\n\
|
|
56
|
+
transfer: "hq-delegate-transfer.sh — make a delegation real everywhere HQ tracks\nownership: board entry, PRD metadata, work mesh, and the project journal.\n\nUsage:\n core/scripts/hq-delegate-transfer.sh --manifest <path>\n\nBehavior (mode \"transfer\"; mode \"share\" skips every mutation):\n 1. companies/<co>/board.json — the project's entry gets owner=<recipient>\n and a bumped updated_at; created (once) if absent. A brainstorm-stage\n entry (status exploring, brainstorm_path set) is matched by\n brainstorm_path and keeps its status.\n 2. prd.json metadata gains owner, delegatedFrom, delegatedAt; for a\n brainstorm-stage project the same three keys land in the\n brainstorm.md frontmatter (owner, delegated_from, delegated_at)\n 3. work-mesh: `done` event closes the delegator's in-progress thread and\n broadcasts the reassignment; silently skipped when the helper is\n unavailable (local/offline installs) — the transfer still succeeds\n 4. a dated \"Delegated\" stanza is appended to the project journal naming\n the recipient, delegation id, and transferred scope\n\nIdempotent: re-running updates owner/timestamps in place — never a\nduplicate board entry, never a duplicated journal stanza.\n\n",
|
|
56
57
|
};
|
|
57
58
|
function usage(name) {
|
|
58
59
|
return USAGE_TEXT[name];
|
|
@@ -189,7 +190,6 @@ export function moveFile(source, target, fileSystem = fs) {
|
|
|
189
190
|
}
|
|
190
191
|
export function writeJsonAtomically(target, value, siblingTemp = false, fileSystem = fs) {
|
|
191
192
|
const serialized = serializeJqJson(value, "pretty");
|
|
192
|
-
// Native delegate JSON writers use LF on every platform.
|
|
193
193
|
if (siblingTemp) {
|
|
194
194
|
const temporary = `${target}.tmp.${randomBytes(6).toString("hex")}`;
|
|
195
195
|
let fd;
|
|
@@ -918,14 +918,25 @@ function runTransfer(args, context) {
|
|
|
918
918
|
}
|
|
919
919
|
const now = jqRawPath(document, "ownershipTransferredAt") || new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
920
920
|
const today = (process.env.HQ_DELEGATE_NOW || new Date().toISOString()).slice(0, 10);
|
|
921
|
+
const stage = jqRawPath(document, "project.stage", "prd");
|
|
921
922
|
const prdPath = `companies/${company}/projects/${project}/prd.json`;
|
|
922
923
|
const prdAbs = path.join(root, prdPath);
|
|
924
|
+
const brainstormPath = `companies/${company}/projects/${project}/brainstorm.md`;
|
|
925
|
+
const brainstormAbs = path.join(root, brainstormPath);
|
|
923
926
|
const projectDir = path.join(root, "companies", company, "projects", project);
|
|
924
927
|
const board = path.join(root, "companies", company, "board.json");
|
|
925
|
-
if (
|
|
928
|
+
if (stage !== "prd" && stage !== "brainstorm") {
|
|
929
|
+
writeErr(`${prefix} manifest project.stage is '${stage}' — expected prd or brainstorm\n`);
|
|
930
|
+
return 1;
|
|
931
|
+
}
|
|
932
|
+
if (stage === "prd" && (!fs.existsSync(prdAbs) || !fs.statSync(prdAbs).isFile())) {
|
|
926
933
|
writeErr(`${prefix} prd not found: ${prdPath}\n`);
|
|
927
934
|
return 1;
|
|
928
935
|
}
|
|
936
|
+
if (stage === "brainstorm" && (!fs.existsSync(brainstormAbs) || !fs.statSync(brainstormAbs).isFile())) {
|
|
937
|
+
writeErr(`${prefix} brainstorm.md not found: ${brainstormPath}\n`);
|
|
938
|
+
return 1;
|
|
939
|
+
}
|
|
929
940
|
if (fs.existsSync(board)) {
|
|
930
941
|
if (!jqValid(board)) {
|
|
931
942
|
writeErr(`${prefix} board.json is not valid JSON: ${board}\n`);
|
|
@@ -936,9 +947,9 @@ function runTransfer(args, context) {
|
|
|
936
947
|
fs.mkdirSync(path.dirname(board), { recursive: true });
|
|
937
948
|
fs.writeFileSync(board, '{"projects": []}\n');
|
|
938
949
|
}
|
|
939
|
-
const prd = readJsonFile(prdAbs);
|
|
940
|
-
const title = jqRawPath(prd, "name");
|
|
941
|
-
const descriptionValue = jqFallback(jsonAt(prd, "description"), "");
|
|
950
|
+
const prd = stage === "prd" ? readJsonFile(prdAbs) : undefined;
|
|
951
|
+
const title = stage === "prd" ? jqRawPath(prd, "name") : brainstormTitle(brainstormAbs);
|
|
952
|
+
const descriptionValue = stage === "prd" ? jqFallback(jsonAt(prd, "description"), "") : brainstormSummary(brainstormAbs);
|
|
942
953
|
const description = Buffer.from(serializeJqJson(descriptionValue, "raw"), "utf8")
|
|
943
954
|
.subarray(0, 300).toString("utf8").replace(/\n+$/, "");
|
|
944
955
|
const boardDocument = readJsonFile(board);
|
|
@@ -948,35 +959,46 @@ function runTransfer(args, context) {
|
|
|
948
959
|
const projects = Array.isArray(projectsValue)
|
|
949
960
|
? projectsValue
|
|
950
961
|
: isRecord(projectsValue) ? Object.values(projectsValue) : [];
|
|
951
|
-
const
|
|
962
|
+
const matchPath = stage === "prd" ? prdPath : brainstormPath;
|
|
963
|
+
const hasProject = projects.some((entry) => jsonAt(entry, stage === "prd" ? "prd_path" : "brainstorm_path") === matchPath);
|
|
952
964
|
if (hasProject) {
|
|
953
|
-
boardDocument.projects = projects.map((entry) => jsonAt(entry, "prd_path") ===
|
|
965
|
+
boardDocument.projects = projects.map((entry) => jsonAt(entry, stage === "prd" ? "prd_path" : "brainstorm_path") === matchPath && isRecord(entry)
|
|
954
966
|
? { ...entry, owner: principal, updated_at: now }
|
|
955
967
|
: entry);
|
|
956
968
|
}
|
|
957
969
|
else {
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
970
|
+
const entry = {
|
|
971
|
+
id: `proj-${project}`,
|
|
972
|
+
title,
|
|
973
|
+
description,
|
|
974
|
+
status: stage === "prd" ? "in_progress" : "exploring",
|
|
975
|
+
owner: principal,
|
|
976
|
+
prd_path: stage === "prd" ? prdPath : null,
|
|
977
|
+
created_at: now,
|
|
978
|
+
updated_at: now,
|
|
979
|
+
};
|
|
980
|
+
if (stage === "brainstorm")
|
|
981
|
+
entry.brainstorm_path = brainstormPath;
|
|
982
|
+
boardDocument.projects = [...projects, entry];
|
|
968
983
|
}
|
|
969
984
|
boardDocument.updated_at = now;
|
|
970
985
|
writeJsonAtomically(board, boardDocument);
|
|
971
|
-
if (
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
prd.metadata
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
986
|
+
if (stage === "prd") {
|
|
987
|
+
if (!isRecord(prd))
|
|
988
|
+
throw new TypeError("Cannot update non-object prd.json");
|
|
989
|
+
if (!isRecord(prd.metadata))
|
|
990
|
+
prd.metadata = {};
|
|
991
|
+
const metadata = prd.metadata;
|
|
992
|
+
metadata.owner = principal;
|
|
993
|
+
metadata.delegatedFrom = from;
|
|
994
|
+
metadata.delegatedAt = now;
|
|
995
|
+
writeJsonAtomically(prdAbs, prd);
|
|
996
|
+
}
|
|
997
|
+
else {
|
|
998
|
+
brainstormFrontmatterSet(brainstormAbs, "owner", principal);
|
|
999
|
+
brainstormFrontmatterSet(brainstormAbs, "delegated_from", from);
|
|
1000
|
+
brainstormFrontmatterSet(brainstormAbs, "delegated_at", now);
|
|
1001
|
+
}
|
|
980
1002
|
if (commandExists("hq", context.cwd)) {
|
|
981
1003
|
const sessionId = process.env.HQ_SESSION_ID || "delegate";
|
|
982
1004
|
const mesh = subprocess("hq", ["mesh", "session", "note", "--enqueue", "--session-id", sessionId, "--seq", "1", "--harness", "claude-code", "--adapter-version", "1.0.0", "--company-slug", company, "--project", project, "--summary", `Delegated to ${display} (${principal}) — delegation ${delegationId}; ownership transferred`], context.cwd, { stdout: "inherit" });
|
|
@@ -993,12 +1015,17 @@ function runTransfer(args, context) {
|
|
|
993
1015
|
const prefixesValue = jqFallback(document.vaultPrefixes, []);
|
|
994
1016
|
const vaultPrefixes = Array.isArray(prefixesValue) ? prefixesValue : [];
|
|
995
1017
|
const scope = vaultPrefixes.map((entry) => jqRawPath(entry, "prefix")).join(", ");
|
|
996
|
-
|
|
1018
|
+
const ownershipLine = stage === "prd"
|
|
1019
|
+
? "Local board and PRD ownership updated; Work Mesh ownership is unconfirmed. Existing delegator access is unchanged."
|
|
1020
|
+
: "Brainstorm-stage project (no PRD yet). Local board and brainstorm.md ownership updated; Work Mesh ownership is unconfirmed. Existing delegator access is unchanged.";
|
|
1021
|
+
fs.appendFileSync(journal, `\n## ${today} — Delegated to ${display}\n\n- Delegation: \`${delegationId}\`\n- From: ${from}\n- To: ${display} (${principal})\n- Transferred scope: ${scope}\n- ${ownershipLine}\n`);
|
|
997
1022
|
}
|
|
998
1023
|
document.ownershipTransferredAt = now;
|
|
999
1024
|
document.workMeshOwnership = "unconfirmed";
|
|
1000
1025
|
writeJsonAtomically(file, document);
|
|
1001
|
-
|
|
1026
|
+
const sourceLabel = stage === "prd" ? "PRD" : "brainstorm.md";
|
|
1027
|
+
const stageLabel = stage === "prd" ? "" : " (brainstorm stage)";
|
|
1028
|
+
writeOut(`${prefix} ownership of '${project}'${stageLabel} transferred to ${display} (${principal}) — local board, ${sourceLabel}, and journal updated; Work Mesh ownership unconfirmed\n`);
|
|
1002
1029
|
return 0;
|
|
1003
1030
|
}
|
|
1004
1031
|
function runNative(name, args, context) {
|
package/dist/commands/core.js
CHANGED
|
@@ -268,6 +268,7 @@ export const SCAFFOLD_ONLY_ASSETS = [
|
|
|
268
268
|
{ asset: "core/scripts/lib/session-scope-capability.sh", root: "live", summary: "Session scope-capability lib sourced by hq-session" },
|
|
269
269
|
{ asset: "core/scripts/lib/session-id.sh", root: "live", summary: "Session-id resolver lib sourced by hq-session" },
|
|
270
270
|
{ asset: "core/scripts/lib/hq-cli-floor.sh", root: "live", summary: "Shared CLI version-floor helper sourced by generated forwarders" },
|
|
271
|
+
{ asset: "core/scripts/lib/delegate-brainstorm.sh", root: "live", summary: "Shared brainstorm-stage data helper sourced by delegate assets" },
|
|
271
272
|
{ asset: "core/scripts/lib/forwarder.sh", root: "live", summary: "Shared runtime sourced by generated scaffold forwarders" },
|
|
272
273
|
{ asset: "core/scripts/lib/work-mesh-enqueue.sh", root: "live", summary: "Work Mesh Live spool enqueue lib sourced by hooks" },
|
|
273
274
|
{ asset: "core/scripts/lib/work-mesh-live-hook.sh", root: "live", summary: "Work Mesh Live hook helpers sourced by rebind" },
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type BotSessionScope } from "./paths.js";
|
|
2
|
+
export declare const DM_HISTORY_MAX_ENTRIES = 40;
|
|
3
|
+
export interface DmHistoryMessage {
|
|
4
|
+
role: "user" | "assistant";
|
|
5
|
+
from: string;
|
|
6
|
+
fromPersonUid?: string;
|
|
7
|
+
body: string;
|
|
8
|
+
at: string;
|
|
9
|
+
eventId?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface DmHistoryFile {
|
|
12
|
+
v: 1;
|
|
13
|
+
messages: DmHistoryMessage[];
|
|
14
|
+
updatedAt: string;
|
|
15
|
+
/** Successful model turns in this DM. Used to inject history only after a session has existed. */
|
|
16
|
+
modelTurns?: number;
|
|
17
|
+
}
|
|
18
|
+
export declare function readDmHistory(dir: string, scope: BotSessionScope): DmHistoryFile;
|
|
19
|
+
export declare function writeDmHistory(dir: string, scope: BotSessionScope, history: DmHistoryFile): void;
|
|
20
|
+
/** Record a message, skipping duplicates by eventId and trimming the oldest past the cap. Pure. */
|
|
21
|
+
export declare function withDmHistoryMessage(history: DmHistoryFile, message: DmHistoryMessage, cap?: number): DmHistoryFile;
|
|
22
|
+
export declare function appendDmHistory(dir: string, scope: BotSessionScope, message: DmHistoryMessage, cap?: number): DmHistoryFile;
|
|
23
|
+
/** Count a successful model turn so a later new session knows there is thread to restore. */
|
|
24
|
+
export declare function markDmHistoryModelTurn(dir: string, scope: BotSessionScope, at: string): void;
|
|
25
|
+
export declare function dmHistoryHasModelThread(history: DmHistoryFile): boolean;
|
|
26
|
+
export declare function formatDmHistoryLines(messages: readonly DmHistoryMessage[], opts: {
|
|
27
|
+
excludeEventId?: string;
|
|
28
|
+
limit: number;
|
|
29
|
+
maxChars: number;
|
|
30
|
+
}): string[];
|
|
31
|
+
/** Prefix a DM turn with a "Recent messages:" block. Empty history leaves the prompt unchanged. */
|
|
32
|
+
export declare function withRecentDmHistory(prompt: string, messages: readonly DmHistoryMessage[], opts: {
|
|
33
|
+
excludeEventId?: string;
|
|
34
|
+
limit: number;
|
|
35
|
+
maxChars: number;
|
|
36
|
+
}): string;
|
|
37
|
+
//# sourceMappingURL=dm-history.d.ts.map
|