@indigoai-us/hq-cli 5.86.0 → 5.88.0

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.
Files changed (32) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/assets/scaffold/core/scripts/lint-shared-worker-skills.sh +143 -0
  3. package/assets/scaffold/core/scripts/share-worker-skill.sh +178 -0
  4. package/dist/commands/core.d.ts +29 -3
  5. package/dist/commands/core.js +121 -24
  6. package/dist/commands/index-cmd.d.ts +4 -0
  7. package/dist/commands/index-cmd.js +34 -0
  8. package/dist/lib/index-render/companies.d.ts +3 -0
  9. package/dist/lib/index-render/companies.js +57 -0
  10. package/dist/lib/index-render/company-knowledge.d.ts +3 -0
  11. package/dist/lib/index-render/company-knowledge.js +85 -0
  12. package/dist/lib/index-render/index.d.ts +5 -0
  13. package/dist/lib/index-render/index.js +43 -0
  14. package/dist/lib/index-render/orchestrator.d.ts +3 -0
  15. package/dist/lib/index-render/orchestrator.js +52 -0
  16. package/dist/lib/index-render/projects.d.ts +3 -0
  17. package/dist/lib/index-render/projects.js +33 -0
  18. package/dist/lib/index-render/public-knowledge.d.ts +3 -0
  19. package/dist/lib/index-render/public-knowledge.js +37 -0
  20. package/dist/lib/index-render/reports.d.ts +3 -0
  21. package/dist/lib/index-render/reports.js +37 -0
  22. package/dist/lib/index-render/shared.d.ts +35 -0
  23. package/dist/lib/index-render/shared.js +126 -0
  24. package/dist/lib/index-render/social-drafts.d.ts +3 -0
  25. package/dist/lib/index-render/social-drafts.js +53 -0
  26. package/dist/lib/index-render/threads.d.ts +3 -0
  27. package/dist/lib/index-render/threads.js +42 -0
  28. package/dist/lib/index-render/workers.d.ts +3 -0
  29. package/dist/lib/index-render/workers.js +49 -0
  30. package/dist/lib/search-index/background.d.ts +39 -0
  31. package/dist/lib/search-index/background.js +382 -0
  32. package/package.json +1 -1
@@ -1,4 +1,6 @@
1
+ import { Option } from 'commander';
1
2
  import { deriveCollections, listRegisteredCollections, reconcileCollections, resolveQmdBin, resolveQmdVersion, runQmd, } from '../lib/search-index/index.js';
3
+ import { backgroundStatus, defaultBackgroundDependencies, runBackgroundLauncher, runBackgroundWorker, } from '../lib/search-index/background.js';
2
4
  import { findHqRoot } from '../utils/manifest.js';
3
5
  const defaults = {
4
6
  reconcileCollections,
@@ -7,6 +9,9 @@ const defaults = {
7
9
  resolveQmdBin,
8
10
  resolveQmdVersion,
9
11
  runQmd,
12
+ runBackgroundLauncher,
13
+ runBackgroundWorker,
14
+ backgroundStatus,
10
15
  };
11
16
  /** Incrementally update qmd, embedding only when an operator explicitly asks. */
12
17
  export function syncSearchIndex(hqRoot, embed, dependencies = defaults) {
@@ -18,6 +23,14 @@ export function syncSearchIndex(hqRoot, embed, dependencies = defaults) {
18
23
  function resolveRoot(hqRoot) {
19
24
  return hqRoot ?? findHqRoot();
20
25
  }
26
+ function makeBackgroundDependencies(hqRoot, dependencies) {
27
+ return {
28
+ ...defaultBackgroundDependencies(hqRoot),
29
+ resolveQmdBin: dependencies.resolveQmdBin,
30
+ reconcileCollections: dependencies.reconcileCollections,
31
+ runQmd: dependencies.runQmd,
32
+ };
33
+ }
21
34
  export function collectionStatusLines(expected, registered) {
22
35
  const expectedNames = new Set(expected.map((collection) => collection.name));
23
36
  const managed = expected.map((collection) => `${registered.has(collection.name) ? 'registered' : 'missing'} ${collection.name} ${collection.path}`);
@@ -54,6 +67,25 @@ export function registerIndexCommand(program, dependencies = defaults) {
54
67
  for (const line of collectionStatusLines(dependencies.deriveCollections(hqRoot), registered))
55
68
  console.log(line);
56
69
  });
70
+ index
71
+ .command('background')
72
+ .description('Run a detached, single-flight qmd cleanup and reindex')
73
+ .option('--log <path>', 'Write worker output to this log file')
74
+ .addOption(new Option('--worker').hideHelp())
75
+ .option('--hq-root <path>', 'HQ root to index (defaults to auto-detected root)')
76
+ .action((options) => {
77
+ const hqRoot = resolveRoot(options.hqRoot);
78
+ const background = makeBackgroundDependencies(hqRoot, dependencies);
79
+ if (options.log)
80
+ background.env = { ...background.env, QMD_REINDEX_LOG: options.log };
81
+ const result = options.worker
82
+ ? (dependencies.runBackgroundWorker ?? runBackgroundWorker)(background)
83
+ : (dependencies.runBackgroundLauncher ?? runBackgroundLauncher)(background);
84
+ if (!options.worker && result.state === 'launched')
85
+ console.log(result.pid);
86
+ else if (!options.worker && (result.state === 'skipped-agent' || result.state === 'skipped'))
87
+ console.log(result.state);
88
+ });
57
89
  index
58
90
  .command('status')
59
91
  .description('Show qmd binary, collection, and index status')
@@ -65,8 +97,10 @@ export function registerIndexCommand(program, dependencies = defaults) {
65
97
  const expected = dependencies.deriveCollections(hqRoot);
66
98
  const qmdStatus = dependencies.runQmd(['status'], { bin, cwd: hqRoot });
67
99
  const qmdVersion = dependencies.resolveQmdVersion();
100
+ const background = (dependencies.backgroundStatus ?? backgroundStatus)(makeBackgroundDependencies(hqRoot, dependencies));
68
101
  console.log(`qmd: ${bin}${qmdVersion ? ` (version ${qmdVersion})` : ''}`);
69
102
  console.log(collectionSummary(expected, registered));
103
+ console.log(`background: lock ${background.lock}; last completed ${background.completedAt ?? 'never'}`);
70
104
  if (qmdStatus.stdout)
71
105
  process.stdout.write(qmdStatus.stdout);
72
106
  if (qmdStatus.stderr)
@@ -0,0 +1,3 @@
1
+ import { RenderContext, RenderResult } from "./shared.js";
2
+ export declare function renderCompanies(context: RenderContext): RenderResult;
3
+ //# sourceMappingURL=companies.d.ts.map
@@ -0,0 +1,57 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { basename, countImmediate, date, exists, immediateEntries, isHidden, log, projectStatus, readJson, sanitize, titleize, truncate, write } from "./shared.js";
4
+ function description(root, company, name) {
5
+ const base = `companies/${company}/${name}`;
6
+ const nonHidden = (child) => !isHidden(child);
7
+ switch (name) {
8
+ case "data": return `${countImmediate(root, base, nonHidden)} item(s)`;
9
+ case "knowledge": return exists(root, `${base}/.git`) ? "Embedded git repo" : `Inline knowledge (${countImmediate(root, base, (n, f) => !isHidden(n) && fs.statSync(f).isDirectory())} subdirs)`;
10
+ case "settings": return `${countImmediate(root, base, (n, f) => !isHidden(n) && fs.statSync(f).isDirectory())} integration(s)`;
11
+ case "policies": return `${countImmediate(root, base, (n) => n.endsWith(".md"))} policy file(s)`;
12
+ case "projects": return `${countImmediate(root, base, (n, f) => !n.startsWith("_") && !isHidden(n) && fs.statSync(f).isDirectory())} project(s)`;
13
+ case "workers": return `${countImmediate(root, base, (n, f) => !isHidden(n) && fs.statSync(f).isDirectory())} worker(s)`;
14
+ case "repos": return `${countImmediate(root, base, (n, f) => { const s = fs.lstatSync(f); return !isHidden(n) && (s.isDirectory() || s.isSymbolicLink()); })} repo link(s)`;
15
+ case "registry": return `Resource registry (${countImmediate(root, `${base}/resources`, (n) => n.endsWith(".yaml"))} resource(s))`;
16
+ case "scripts": return `${countImmediate(root, base, (_n, f) => fs.statSync(f).isFile())} script(s)`;
17
+ default: return "—";
18
+ }
19
+ }
20
+ export function renderCompanies(context) {
21
+ const written = [];
22
+ for (const directory of immediateEntries(context.root, "companies", "dir")) {
23
+ const company = basename(directory);
24
+ if (company.startsWith("_") || isHidden(company))
25
+ continue;
26
+ const lines = [`# ${titleize(company)}`, "", `> Auto-generated. Updated: ${date(context.now)}`, "", "| Name | Description |", "|------|-------------|"];
27
+ for (const name of ["data", "knowledge", "policies", "projects", "registry", "repos", "scripts", "settings", "workers"]) {
28
+ if (exists(context.root, `companies/${company}/${name}`))
29
+ lines.push(`| \`${name}/\` | ${sanitize(description(context.root, company, name))} |`);
30
+ }
31
+ lines.push("", "## Projects", "");
32
+ const projects = `companies/${company}/projects`;
33
+ if (exists(context.root, projects)) {
34
+ lines.push("| Project | Status | Description |", "|---------|--------|-------------|");
35
+ for (const projectDirectory of immediateEntries(context.root, projects, "dir")) {
36
+ const project = basename(projectDirectory);
37
+ if (project.startsWith("_") || isHidden(project))
38
+ continue;
39
+ const prd = path.join(projectDirectory, "prd.json");
40
+ const parsed = readJson(prd);
41
+ let text = typeof parsed?.description === "string" ? parsed.description : "";
42
+ text = truncate(sanitize(text), 110) || "—";
43
+ lines.push(`| \`${project}\` | ${projectStatus(context.root, project, prd, "active")} | ${text} |`);
44
+ }
45
+ }
46
+ else
47
+ lines.push("_No projects directory._");
48
+ lines.push("", "## Deployments", "", "_See `companies/manifest.yaml` for deployment targets._", "");
49
+ const output = `companies/${company}/INDEX.md`;
50
+ write(context, output, lines.join("\n"));
51
+ written.push(output);
52
+ log(context, `rebuild-companies-index: wrote ${output}`);
53
+ }
54
+ log(context, `rebuild-companies-index: regenerated ${written.length} company INDEX.md file(s)`);
55
+ return { written };
56
+ }
57
+ //# sourceMappingURL=companies.js.map
@@ -0,0 +1,3 @@
1
+ import { RenderContext, RenderResult } from "./shared.js";
2
+ export declare function renderCompanyKnowledge(context: RenderContext): RenderResult;
3
+ //# sourceMappingURL=company-knowledge.d.ts.map
@@ -0,0 +1,85 @@
1
+ import { basename, date, immediateEntries, isHidden, log, readJson, readText, sanitize, titleize, truncate, write } from "./shared.js";
2
+ function describe(item) {
3
+ const name = basename(item);
4
+ // `find -L` follows a symlink only for its direct target. fs.statSync mirrors that.
5
+ try {
6
+ if ((awaitStat(item)).isDirectory()) {
7
+ const fs = requireFs();
8
+ return `${fs.readdirSync(item).filter((entry) => !entry.startsWith(".")).length} item(s)`;
9
+ }
10
+ }
11
+ catch {
12
+ return name;
13
+ }
14
+ if (name.endsWith(".md"))
15
+ return readText(item)?.match(/^# +(.+)$/m)?.[1] || name;
16
+ if (name.endsWith(".yaml") || name.endsWith(".yml")) {
17
+ const found = readText(item)?.match(/^description:\s*(.*)$/m)?.[1]?.replace(/^["']|["']$/g, "");
18
+ return found || name;
19
+ }
20
+ if (name.endsWith(".json")) {
21
+ const parsed = readJson(item);
22
+ return typeof parsed?.description === "string" ? parsed.description : typeof parsed?.name === "string" ? parsed.name : name;
23
+ }
24
+ return name;
25
+ }
26
+ // Avoid a heavier tree abstraction here: the source script's `find -L` is
27
+ // specifically about knowledge gitlinks, so stat is intentionally local.
28
+ function requireFs() { return fsModule; }
29
+ import * as fsModule from "fs";
30
+ function awaitStat(file) { return fsModule.statSync(file); }
31
+ export function renderCompanyKnowledge(context) {
32
+ const written = [];
33
+ for (const companyDir of immediateEntries(context.root, "companies", "dir")) {
34
+ const company = basename(companyDir);
35
+ if (company.startsWith("_") || isHidden(company))
36
+ continue;
37
+ const relative = `companies/${company}/knowledge`;
38
+ const knowledge = `${companyDir}/knowledge`;
39
+ try {
40
+ if (!fsModule.statSync(knowledge).isDirectory())
41
+ continue;
42
+ }
43
+ catch {
44
+ continue;
45
+ }
46
+ const entries = fsModule.readdirSync(knowledge).flatMap((name) => {
47
+ const item = `${knowledge}/${name}`;
48
+ try {
49
+ return fsModule.statSync(item).isDirectory() ? [item] : [];
50
+ }
51
+ catch {
52
+ return [];
53
+ }
54
+ }).sort().concat(fsModule.readdirSync(knowledge).flatMap((name) => {
55
+ const item = `${knowledge}/${name}`;
56
+ try {
57
+ return fsModule.statSync(item).isFile() ? [item] : [];
58
+ }
59
+ catch {
60
+ return [];
61
+ }
62
+ }).sort());
63
+ const lines = [`# ${titleize(company)} Knowledge`, "", `> Auto-generated. Updated: ${date(context.now)}`, "", "| Name | Description |", "|------|-------------|"];
64
+ for (const item of entries) {
65
+ const name = basename(item);
66
+ if (isHidden(name) || name === "INDEX.md")
67
+ continue;
68
+ const value = truncate(sanitize(describe(item)), 100) || "—";
69
+ let directory = false;
70
+ try {
71
+ directory = fsModule.statSync(item).isDirectory();
72
+ }
73
+ catch { /* omitted */ }
74
+ lines.push(`| \`${name}${directory ? "/" : ""}\` | ${value} |`);
75
+ }
76
+ lines.push("");
77
+ const output = `${relative}/INDEX.md`;
78
+ write(context, output, lines.join("\n"));
79
+ written.push(output);
80
+ log(context, `rebuild-company-knowledge-index: wrote ${output}`);
81
+ }
82
+ log(context, `rebuild-company-knowledge-index: regenerated ${written.length} knowledge INDEX.md file(s)`);
83
+ return { written };
84
+ }
85
+ //# sourceMappingURL=company-knowledge.js.map
@@ -0,0 +1,5 @@
1
+ import { RenderContext, RenderResult } from "./shared.js";
2
+ export type IndexTarget = "all" | "companies" | "company-knowledge" | "orchestrator" | "projects" | "public-knowledge" | "reports" | "social-drafts" | "threads" | "workers";
3
+ export declare function renderIndexTarget(target: IndexTarget, context: RenderContext, args?: string[]): RenderResult;
4
+ export { atomicWrite, comparePath, immediateEntries, type RenderContext, type RenderResult } from "./shared.js";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,43 @@
1
+ import { renderCompanies } from "./companies.js";
2
+ import { renderCompanyKnowledge } from "./company-knowledge.js";
3
+ import { renderOrchestrator } from "./orchestrator.js";
4
+ import { renderProjects } from "./projects.js";
5
+ import { renderPublicKnowledge } from "./public-knowledge.js";
6
+ import { renderReports } from "./reports.js";
7
+ import { renderSocialDrafts } from "./social-drafts.js";
8
+ import { renderThreads } from "./threads.js";
9
+ import { renderWorkers } from "./workers.js";
10
+ const allOrder = ["threads", "orchestrator", "companies", "projects", "company-knowledge", "public-knowledge", "workers", "reports", "social-drafts"];
11
+ export function renderIndexTarget(target, context, args = []) {
12
+ switch (target) {
13
+ case "companies": return renderCompanies(context);
14
+ case "company-knowledge": return renderCompanyKnowledge(context);
15
+ case "orchestrator": return renderOrchestrator(context);
16
+ case "projects": return renderProjects(context);
17
+ case "public-knowledge": return renderPublicKnowledge(context);
18
+ case "reports": return renderReports(context);
19
+ case "social-drafts": return renderSocialDrafts(context);
20
+ case "threads": return renderThreads(context, args);
21
+ case "workers": return renderWorkers(context);
22
+ case "all": {
23
+ const written = [];
24
+ let errors = 0;
25
+ for (const child of allOrder) {
26
+ try {
27
+ written.push(...renderIndexTarget(child, context).written);
28
+ }
29
+ catch (_error) {
30
+ errors += 1;
31
+ context.log?.(`rebuild-all-indexes: ERROR in rebuild-${child}-index.sh`);
32
+ }
33
+ }
34
+ // The shell sorts and deduplicates the `wrote` paths before JSON output.
35
+ const stdoutPaths = written.filter((file) => /^(workspace|companies|knowledge|workers|projects)/.test(file));
36
+ (context.output ?? ((message) => process.stdout.write(message)))(`${JSON.stringify([...new Set(stdoutPaths)].sort(), null, 2)}\n`);
37
+ context.log?.(errors ? `all-indexes: errors (${errors})` : "all-indexes: ok");
38
+ return { written };
39
+ }
40
+ }
41
+ }
42
+ export { atomicWrite, comparePath, immediateEntries } from "./shared.js";
43
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,3 @@
1
+ import { RenderContext, RenderResult } from "./shared.js";
2
+ export declare function renderOrchestrator(context: RenderContext): RenderResult;
3
+ //# sourceMappingURL=orchestrator.d.ts.map
@@ -0,0 +1,52 @@
1
+ import * as fs from "fs";
2
+ import { at, log, readJson, timestamp, write } from "./shared.js";
3
+ function stateFiles(root) {
4
+ const base = at(root, "workspace/orchestrator");
5
+ try {
6
+ // The source intentionally does not sort this `find`; opendir's iterator
7
+ // preserves the directory stream order rather than readdirSync's sorting.
8
+ const stream = fs.opendirSync(base);
9
+ const entries = [];
10
+ try {
11
+ for (let entry = stream.readSync(); entry; entry = stream.readSync()) {
12
+ if (entry.name === "_archive" || entry.name === "_pipeline")
13
+ continue;
14
+ const state = `${base}/${entry.name}/state.json`;
15
+ if (fs.existsSync(state))
16
+ entries.push(state);
17
+ }
18
+ }
19
+ finally {
20
+ stream.closeSync();
21
+ }
22
+ return entries;
23
+ }
24
+ catch {
25
+ return [];
26
+ }
27
+ }
28
+ function scalar(value, fallback) { return typeof value === "string" ? value : fallback; }
29
+ export function renderOrchestrator(context) {
30
+ fs.mkdirSync(at(context.root, "workspace/orchestrator"), { recursive: true });
31
+ const rows = [];
32
+ for (const file of stateFiles(context.root)) {
33
+ const state = readJson(file);
34
+ if (!state) {
35
+ log(context, `rebuild-orchestrator-index: WARNING: skipped unparseable ${file}: invalid JSON`);
36
+ continue;
37
+ }
38
+ if (["completed", "complete"].includes(scalar(state.status, "")) || scalar(state.phase, "") === "complete")
39
+ continue;
40
+ const source = scalar(state.prd_path, scalar(state.scope, ""));
41
+ const company = source.match(/companies\/([^/]+)\//)?.[1] ?? "-";
42
+ const progress = state.progress && typeof state.progress === "object" && !Array.isArray(state.progress)
43
+ ? `${typeof state.progress.completed === "number" ? state.progress.completed : 0}/${typeof state.progress.total === "number" ? state.progress.total : 0}` : "-";
44
+ rows.push(`| ${scalar(state.project, scalar(state.run_id, "-"))} | ${company} | ${scalar(state.status, "-")} | ${progress} | ${scalar(state.updated_at, "-")} |`);
45
+ }
46
+ const output = "workspace/orchestrator/INDEX.md";
47
+ const text = ["# Orchestrator Projects", "", `Generated: ${timestamp(context.now)}`, "", "Active runs only. Archived runs in `workspace/orchestrator/_archive/`.", "", "| Project | Company | Status | Progress | Last Updated |", "|---------|---------|--------|----------|--------------|", ...rows, ""].join("\n");
48
+ write(context, output, text);
49
+ log(context, `rebuild-orchestrator-index: wrote ${output} (${rows.length} active)`);
50
+ return { written: [output] };
51
+ }
52
+ //# sourceMappingURL=orchestrator.js.map
@@ -0,0 +1,3 @@
1
+ import { RenderContext, RenderResult } from "./shared.js";
2
+ export declare function renderProjects(context: RenderContext): RenderResult;
3
+ //# sourceMappingURL=projects.d.ts.map
@@ -0,0 +1,33 @@
1
+ import * as path from "path";
2
+ import { basename, date, exists, immediateEntries, isHidden, log, projectStatus, readJson, sanitize, truncate, write } from "./shared.js";
3
+ export function renderProjects(context) {
4
+ const base = "personal/projects";
5
+ if (!exists(context.root, base)) {
6
+ log(context, "rebuild-projects-index: personal/projects/ missing, skipping");
7
+ return { written: [] };
8
+ }
9
+ const lines = ["# Projects", "", `> Auto-generated. Updated: ${date(context.now)}`, "", "Personal/HQ projects only. Company-scoped projects live in `companies/{co}/projects/`.", "", "## Company Project Directories", "", "| Company | Path |", "|---------|------|"];
10
+ for (const companyDirectory of immediateEntries(context.root, "companies", "dir")) {
11
+ const company = basename(companyDirectory);
12
+ if (!company.startsWith("_") && !isHidden(company) && exists(context.root, `companies/${company}/projects`))
13
+ lines.push(`| ${company} | \`companies/${company}/projects/\` |`);
14
+ }
15
+ lines.push("", "## Personal/HQ Projects", "", "Projects here are HQ infrastructure or cross-company tools.", "", "| Project | Stories | Status | Description |", "|---------|---------|--------|-------------|");
16
+ const projects = immediateEntries(context.root, base, "dir");
17
+ for (const directory of projects) {
18
+ const name = basename(directory);
19
+ if (name.startsWith("_") || isHidden(name))
20
+ continue;
21
+ const prd = path.join(directory, "prd.json");
22
+ const parsed = readJson(prd);
23
+ const stories = Array.isArray(parsed?.userStories) && parsed.userStories.length > 0 ? String(parsed.userStories.length) : "—";
24
+ const description = truncate(sanitize(typeof parsed?.description === "string" ? parsed.description : ""), 110) || "—";
25
+ lines.push(`| \`${name}/\` | ${stories} | ${projectStatus(context.root, name, prd, "—")} | ${description} |`);
26
+ }
27
+ lines.push("");
28
+ const output = `${base}/INDEX.md`;
29
+ write(context, output, lines.join("\n"));
30
+ log(context, `rebuild-projects-index: wrote ${output} (${projects.length} HQ project(s))`);
31
+ return { written: [output] };
32
+ }
33
+ //# sourceMappingURL=projects.js.map
@@ -0,0 +1,3 @@
1
+ import { RenderContext, RenderResult } from "./shared.js";
2
+ export declare function renderPublicKnowledge(context: RenderContext): RenderResult;
3
+ //# sourceMappingURL=public-knowledge.d.ts.map
@@ -0,0 +1,37 @@
1
+ import { basename, countImmediate, date, exists, heading, immediateEntries, isHidden, log, sanitize, truncate, write } from "./shared.js";
2
+ export function renderPublicKnowledge(context) {
3
+ const base = "core/knowledge/public";
4
+ if (!exists(context.root, base)) {
5
+ log(context, "rebuild-public-knowledge-index: core/knowledge/public/ missing, skipping");
6
+ return { written: [] };
7
+ }
8
+ const directories = immediateEntries(context.root, base, "dir");
9
+ const lines = ["# Public Knowledge", "", `> Auto-generated. Updated: ${date(context.now)}`, "", "| Name | Description |", "|------|-------------|"];
10
+ for (const directory of directories) {
11
+ const name = basename(directory);
12
+ if (isHidden(name))
13
+ continue;
14
+ const relative = `${base}/${name}`;
15
+ const count = countImmediate(context.root, relative, (child) => child.endsWith(".md") && child !== "INDEX.md");
16
+ let value = heading(`${directory}/README.md`);
17
+ if (!value) {
18
+ const first = immediateEntries(context.root, relative, "file").filter((file) => file.endsWith(".md") && !file.endsWith("/INDEX.md") && !file.endsWith("/README.md"))[0];
19
+ if (first)
20
+ value = heading(first);
21
+ }
22
+ value = value ? `${value} (${count} file(s))` : `${count} file(s)`;
23
+ lines.push(`| \`${name}/\` | ${truncate(sanitize(value), 100)} |`);
24
+ }
25
+ for (const file of immediateEntries(context.root, base, "file").filter((item) => item.endsWith(".md"))) {
26
+ const name = basename(file);
27
+ if (name === "INDEX.md" || isHidden(name))
28
+ continue;
29
+ lines.push(`| \`${name}\` | ${truncate(sanitize(heading(file) || name), 100)} |`);
30
+ }
31
+ lines.push("");
32
+ const output = `${base}/INDEX.md`;
33
+ write(context, output, lines.join("\n"));
34
+ log(context, `rebuild-public-knowledge-index: wrote ${output} (${directories.length} subdir(s))`);
35
+ return { written: [output] };
36
+ }
37
+ //# sourceMappingURL=public-knowledge.js.map
@@ -0,0 +1,3 @@
1
+ import { RenderContext, RenderResult } from "./shared.js";
2
+ export declare function renderReports(context: RenderContext): RenderResult;
3
+ //# sourceMappingURL=reports.d.ts.map
@@ -0,0 +1,37 @@
1
+ import { basename, countImmediate, date, exists, heading, immediateEntries, isHidden, log, readText, sanitize, truncate, write } from "./shared.js";
2
+ function reportDescription(file) {
3
+ const name = basename(file);
4
+ if (name.endsWith(".md"))
5
+ return heading(file) || name.slice(0, -3);
6
+ if (/\.html?$/.test(name))
7
+ return readText(file)?.match(/<title>(.*?)<\/title>/i)?.[1] || name.replace(/\.[^.]+$/, "");
8
+ return name;
9
+ }
10
+ function reportDate(name) { return name.match(/^(\d{4}-(?:\d{2}-\d{2}|W\d{2}))/)?.[1] ?? "—"; }
11
+ export function renderReports(context) {
12
+ const base = "workspace/reports";
13
+ if (!exists(context.root, base)) {
14
+ log(context, "rebuild-reports-index: workspace/reports/ missing, skipping");
15
+ return { written: [] };
16
+ }
17
+ const lines = ["# Reports", "", `> Auto-generated. Updated: ${date(context.now)}`, "", "Personal/HQ reports only. Company reports moved to `companies/{co}/data/reports/`.", "", "## Directories", "", "| Name | Description |", "|------|-------------|"];
18
+ for (const directory of immediateEntries(context.root, base, "dir")) {
19
+ const name = basename(directory);
20
+ if (!isHidden(name))
21
+ lines.push(`| \`${name}/\` | ${countImmediate(context.root, `${base}/${name}`, (child) => !isHidden(child))} item(s) |`);
22
+ }
23
+ lines.push("", "## Individual Reports", "", "| Name | Date | Description |", "|------|------|-------------|");
24
+ const files = immediateEntries(context.root, base, "file");
25
+ for (const file of files) {
26
+ const name = basename(file);
27
+ if (name === "INDEX.md" || isHidden(name))
28
+ continue;
29
+ lines.push(`| \`${name}\` | ${reportDate(name)} | ${truncate(sanitize(reportDescription(file)), 100) || "—"} |`);
30
+ }
31
+ lines.push("");
32
+ const output = `${base}/INDEX.md`;
33
+ write(context, output, lines.join("\n"));
34
+ log(context, `rebuild-reports-index: wrote ${output} (${files.filter((file) => basename(file) !== "INDEX.md").length} individual report(s))`);
35
+ return { written: [output] };
36
+ }
37
+ //# sourceMappingURL=reports.js.map
@@ -0,0 +1,35 @@
1
+ export type RenderContext = {
2
+ root: string;
3
+ now?: Date;
4
+ log?: (message: string) => void;
5
+ output?: (message: string) => void;
6
+ };
7
+ export type RenderResult = {
8
+ written: string[];
9
+ };
10
+ export declare function rel(root: string, file: string): string;
11
+ export declare function at(root: string, relative: string): string;
12
+ export declare function exists(root: string, relative: string): boolean;
13
+ /** Bytewise, locale-independent ordering matching `find … | sort`. */
14
+ export declare function comparePath(a: string, b: string): number;
15
+ export declare function immediateEntries(root: string, relative: string, kind: "dir" | "file", followSymlinks?: boolean): string[];
16
+ export declare function countImmediate(root: string, relative: string, predicate: (name: string, file: string) => boolean): number;
17
+ export declare function readText(file: string): string | undefined;
18
+ export declare function readJson(file: string): Record<string, unknown> | undefined;
19
+ export declare function heading(file: string): string;
20
+ export declare function sanitize(value: unknown): string;
21
+ /** Bash's character-count truncation; filenames and fixture text are UTF-8. */
22
+ export declare function truncate(value: string, length: number): string;
23
+ export declare function titleize(slug: string): string;
24
+ export declare function date(now?: Date): string;
25
+ export declare function timestamp(now?: Date): string;
26
+ /** Write via a same-directory temporary file, then atomically replace the destination. */
27
+ export declare function atomicWrite(file: string, content: string): void;
28
+ export declare function write(context: RenderContext, relative: string, content: string): string;
29
+ export declare function log(context: RenderContext, message: string): void;
30
+ export declare function projectStatus(root: string, project: string, prdPath: string, fallback: string): string;
31
+ export declare function basename(file: string): string;
32
+ export declare function isHidden(name: string): boolean;
33
+ export declare function mtime(file: string): number;
34
+ export declare function tempDirectory(prefix: string): string;
35
+ //# sourceMappingURL=shared.d.ts.map
@@ -0,0 +1,126 @@
1
+ import * as fs from "fs";
2
+ import * as os from "os";
3
+ import * as path from "path";
4
+ export function rel(root, file) {
5
+ return path.relative(root, file).split(path.sep).join("/");
6
+ }
7
+ export function at(root, relative) {
8
+ return path.join(root, ...relative.split("/"));
9
+ }
10
+ export function exists(root, relative) {
11
+ return fs.existsSync(at(root, relative));
12
+ }
13
+ /** Bytewise, locale-independent ordering matching `find … | sort`. */
14
+ export function comparePath(a, b) {
15
+ return a < b ? -1 : a > b ? 1 : 0;
16
+ }
17
+ export function immediateEntries(root, relative, kind, followSymlinks = false) {
18
+ const base = at(root, relative);
19
+ try {
20
+ return fs.readdirSync(base).flatMap((name) => {
21
+ const candidate = path.join(base, name);
22
+ try {
23
+ const stat = followSymlinks ? fs.statSync(candidate) : fs.lstatSync(candidate);
24
+ return (kind === "dir" ? stat.isDirectory() : stat.isFile()) ? [candidate] : [];
25
+ }
26
+ catch {
27
+ return [];
28
+ }
29
+ }).sort(comparePath);
30
+ }
31
+ catch {
32
+ return [];
33
+ }
34
+ }
35
+ export function countImmediate(root, relative, predicate) {
36
+ try {
37
+ return fs.readdirSync(at(root, relative)).filter((name) => predicate(name, at(root, `${relative}/${name}`))).length;
38
+ }
39
+ catch {
40
+ return 0;
41
+ }
42
+ }
43
+ export function readText(file) {
44
+ try {
45
+ return fs.readFileSync(file, "utf8");
46
+ }
47
+ catch {
48
+ return undefined;
49
+ }
50
+ }
51
+ export function readJson(file) {
52
+ const text = readText(file);
53
+ if (text === undefined)
54
+ return undefined;
55
+ try {
56
+ const parsed = JSON.parse(text);
57
+ return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)
58
+ ? parsed : undefined;
59
+ }
60
+ catch {
61
+ return undefined;
62
+ }
63
+ }
64
+ export function heading(file) {
65
+ return readText(file)?.match(/^# +(.+)$/m)?.[1] ?? "";
66
+ }
67
+ export function sanitize(value) {
68
+ return String(value ?? "").replace(/\n/g, " ").replace(/\|/g, "\\|").replace(/ */g, " ").trim();
69
+ }
70
+ /** Bash's character-count truncation; filenames and fixture text are UTF-8. */
71
+ export function truncate(value, length) {
72
+ return value.length <= length ? value : `${value.slice(0, length - 1)}…`;
73
+ }
74
+ export function titleize(slug) {
75
+ return slug.split("-").map((part) => part ? `${part[0].toUpperCase()}${part.slice(1)}` : "").join(" ");
76
+ }
77
+ export function date(now = new Date()) { return now.toISOString().slice(0, 10); }
78
+ export function timestamp(now = new Date()) { return now.toISOString().replace(/\.\d{3}Z$/, "Z"); }
79
+ /** Write via a same-directory temporary file, then atomically replace the destination. */
80
+ export function atomicWrite(file, content) {
81
+ fs.mkdirSync(path.dirname(file), { recursive: true });
82
+ const temporary = path.join(path.dirname(file), `.${path.basename(file)}.${process.pid}.${Math.random().toString(16).slice(2)}.tmp`);
83
+ try {
84
+ fs.writeFileSync(temporary, content, "utf8");
85
+ fs.renameSync(temporary, file);
86
+ }
87
+ catch (error) {
88
+ try {
89
+ fs.unlinkSync(temporary);
90
+ }
91
+ catch { /* best-effort cleanup */ }
92
+ throw error;
93
+ }
94
+ }
95
+ export function write(context, relative, content) {
96
+ atomicWrite(at(context.root, relative), content);
97
+ return relative;
98
+ }
99
+ export function log(context, message) { context.log?.(message); }
100
+ export function projectStatus(root, project, prdPath, fallback) {
101
+ const state = readJson(at(root, `workspace/orchestrator/${project}/state.json`));
102
+ if (state) {
103
+ const raw = typeof state.status === "string" ? state.status : "active";
104
+ if (["completed", "complete"].includes(raw))
105
+ return "complete";
106
+ if (["in_progress", "active", "started"].includes(raw))
107
+ return "active";
108
+ return raw;
109
+ }
110
+ const prd = readJson(prdPath);
111
+ if (prd && Array.isArray(prd.userStories) && prd.userStories.length > 0 && prd.userStories.every((story) => Boolean(story.passes)))
112
+ return "complete";
113
+ return fallback;
114
+ }
115
+ export function basename(file) { return path.basename(file); }
116
+ export function isHidden(name) { return name.startsWith("."); }
117
+ export function mtime(file) { try {
118
+ return fs.statSync(file).mtimeMs / 1000;
119
+ }
120
+ catch {
121
+ return 0;
122
+ } }
123
+ // Kept exported for primitive tests and consumers that need a temp base without
124
+ // relying on an application-specific fixture location.
125
+ export function tempDirectory(prefix) { return fs.mkdtempSync(path.join(os.tmpdir(), prefix)); }
126
+ //# sourceMappingURL=shared.js.map
@@ -0,0 +1,3 @@
1
+ import { RenderContext, RenderResult } from "./shared.js";
2
+ export declare function renderSocialDrafts(context: RenderContext): RenderResult;
3
+ //# sourceMappingURL=social-drafts.d.ts.map