sentinelayer-cli 0.1.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 (124) hide show
  1. package/README.md +996 -0
  2. package/bin/create-sentinelayer.js +5 -0
  3. package/bin/sentinelayer-cli.js +5 -0
  4. package/bin/sl.js +5 -0
  5. package/package.json +54 -0
  6. package/src/agents/jules/config/definition.js +209 -0
  7. package/src/agents/jules/config/system-prompt.js +175 -0
  8. package/src/agents/jules/error-intake.js +51 -0
  9. package/src/agents/jules/fix-cycle.js +377 -0
  10. package/src/agents/jules/loop.js +367 -0
  11. package/src/agents/jules/pulse.js +319 -0
  12. package/src/agents/jules/stream.js +186 -0
  13. package/src/agents/jules/swarm/file-scanner.js +74 -0
  14. package/src/agents/jules/swarm/index.js +11 -0
  15. package/src/agents/jules/swarm/orchestrator.js +362 -0
  16. package/src/agents/jules/swarm/pattern-hunter.js +123 -0
  17. package/src/agents/jules/swarm/sub-agent.js +308 -0
  18. package/src/agents/jules/tools/auth-audit.js +222 -0
  19. package/src/agents/jules/tools/dispatch.js +327 -0
  20. package/src/agents/jules/tools/file-edit.js +180 -0
  21. package/src/agents/jules/tools/file-read.js +100 -0
  22. package/src/agents/jules/tools/frontend-analyze.js +570 -0
  23. package/src/agents/jules/tools/glob.js +168 -0
  24. package/src/agents/jules/tools/grep.js +228 -0
  25. package/src/agents/jules/tools/index.js +29 -0
  26. package/src/agents/jules/tools/path-guards.js +161 -0
  27. package/src/agents/jules/tools/runtime-audit.js +409 -0
  28. package/src/agents/jules/tools/shell.js +383 -0
  29. package/src/ai/aidenid.js +945 -0
  30. package/src/ai/client.js +508 -0
  31. package/src/ai/domain-target-store.js +268 -0
  32. package/src/ai/identity-store.js +270 -0
  33. package/src/ai/site-store.js +145 -0
  34. package/src/audit/agents/architecture.js +180 -0
  35. package/src/audit/agents/compliance.js +179 -0
  36. package/src/audit/agents/documentation.js +165 -0
  37. package/src/audit/agents/performance.js +145 -0
  38. package/src/audit/agents/security.js +215 -0
  39. package/src/audit/agents/testing.js +172 -0
  40. package/src/audit/orchestrator.js +557 -0
  41. package/src/audit/package.js +204 -0
  42. package/src/audit/registry.js +284 -0
  43. package/src/audit/replay.js +103 -0
  44. package/src/auth/http.js +113 -0
  45. package/src/auth/service.js +848 -0
  46. package/src/auth/session-store.js +345 -0
  47. package/src/cli.js +244 -0
  48. package/src/commands/ai/identity-lifecycle.js +1337 -0
  49. package/src/commands/ai/provision-governance.js +1246 -0
  50. package/src/commands/ai/shared.js +147 -0
  51. package/src/commands/ai.js +11 -0
  52. package/src/commands/apply.js +19 -0
  53. package/src/commands/audit.js +1147 -0
  54. package/src/commands/auth.js +366 -0
  55. package/src/commands/chat.js +191 -0
  56. package/src/commands/config.js +184 -0
  57. package/src/commands/cost.js +311 -0
  58. package/src/commands/daemon/core.js +850 -0
  59. package/src/commands/daemon/extended.js +1048 -0
  60. package/src/commands/daemon/shared.js +213 -0
  61. package/src/commands/daemon.js +11 -0
  62. package/src/commands/guide.js +174 -0
  63. package/src/commands/ingest.js +58 -0
  64. package/src/commands/init.js +55 -0
  65. package/src/commands/legacy-args.js +30 -0
  66. package/src/commands/mcp.js +404 -0
  67. package/src/commands/omargate.js +21 -0
  68. package/src/commands/persona.js +27 -0
  69. package/src/commands/plugin.js +260 -0
  70. package/src/commands/policy.js +132 -0
  71. package/src/commands/prompt.js +238 -0
  72. package/src/commands/review.js +704 -0
  73. package/src/commands/scan.js +788 -0
  74. package/src/commands/spec.js +716 -0
  75. package/src/commands/swarm.js +651 -0
  76. package/src/commands/telemetry.js +202 -0
  77. package/src/commands/watch.js +510 -0
  78. package/src/config/agent-dictionary.js +182 -0
  79. package/src/config/io.js +56 -0
  80. package/src/config/paths.js +18 -0
  81. package/src/config/schema.js +55 -0
  82. package/src/config/service.js +184 -0
  83. package/src/cost/budget.js +235 -0
  84. package/src/cost/history.js +188 -0
  85. package/src/cost/tracker.js +171 -0
  86. package/src/daemon/artifact-lineage.js +534 -0
  87. package/src/daemon/assignment-ledger.js +770 -0
  88. package/src/daemon/ast-parser-layer.js +258 -0
  89. package/src/daemon/budget-governor.js +633 -0
  90. package/src/daemon/callgraph-overlay.js +646 -0
  91. package/src/daemon/error-worker.js +626 -0
  92. package/src/daemon/hybrid-mapper.js +929 -0
  93. package/src/daemon/jira-lifecycle.js +632 -0
  94. package/src/daemon/operator-control.js +657 -0
  95. package/src/daemon/reliability-lane.js +471 -0
  96. package/src/daemon/watchdog.js +971 -0
  97. package/src/guide/generator.js +316 -0
  98. package/src/ingest/engine.js +918 -0
  99. package/src/legacy-cli.js +2435 -0
  100. package/src/mcp/registry.js +695 -0
  101. package/src/memory/blackboard.js +301 -0
  102. package/src/memory/retrieval.js +581 -0
  103. package/src/plugin/manifest.js +553 -0
  104. package/src/policy/packs.js +144 -0
  105. package/src/prompt/generator.js +106 -0
  106. package/src/review/ai-review.js +669 -0
  107. package/src/review/local-review.js +1284 -0
  108. package/src/review/replay.js +235 -0
  109. package/src/review/report.js +664 -0
  110. package/src/review/spec-binding.js +487 -0
  111. package/src/scan/generator.js +351 -0
  112. package/src/spec/generator.js +519 -0
  113. package/src/spec/regenerate.js +237 -0
  114. package/src/spec/templates.js +91 -0
  115. package/src/swarm/dashboard.js +247 -0
  116. package/src/swarm/factory.js +363 -0
  117. package/src/swarm/pentest.js +934 -0
  118. package/src/swarm/registry.js +419 -0
  119. package/src/swarm/report.js +158 -0
  120. package/src/swarm/runtime.js +576 -0
  121. package/src/swarm/scenario-dsl.js +272 -0
  122. package/src/telemetry/ledger.js +302 -0
  123. package/src/ui/markdown.js +220 -0
  124. package/src/ui/progress.js +100 -0
@@ -0,0 +1,260 @@
1
+ import path from "node:path";
2
+ import process from "node:process";
3
+
4
+ import pc from "picocolors";
5
+
6
+ import {
7
+ buildPluginManifestTemplate,
8
+ computePluginLoadOrder,
9
+ listPluginManifests,
10
+ normalizePluginId,
11
+ normalizePluginLoadStage,
12
+ normalizePluginPackType,
13
+ PLUGIN_LOAD_STAGES,
14
+ PLUGIN_PACK_TYPES,
15
+ readJsonFile,
16
+ resolveDefaultPluginManifestPath,
17
+ summarizePluginValidationError,
18
+ validatePluginManifest,
19
+ writeJsonFile,
20
+ } from "../plugin/manifest.js";
21
+
22
+ function shouldEmitJson(options, command) {
23
+ const local = Boolean(options && options.json);
24
+ const globalFromCommand =
25
+ command && command.optsWithGlobals ? Boolean(command.optsWithGlobals().json) : false;
26
+ return local || globalFromCommand;
27
+ }
28
+
29
+ function normalizeOutputPath(rawValue, fallbackPath) {
30
+ const candidate = String(rawValue || "").trim();
31
+ if (!candidate) {
32
+ return fallbackPath;
33
+ }
34
+ return path.resolve(process.cwd(), candidate);
35
+ }
36
+
37
+ export function registerPluginCommand(program) {
38
+ const plugin = program
39
+ .command("plugin")
40
+ .description("Manage Sentinelayer plugin manifests and extension-pack contracts");
41
+
42
+ plugin
43
+ .command("init")
44
+ .description("Initialize a deterministic plugin manifest scaffold")
45
+ .requiredOption("--id <plugin-id>", "Unique plugin id (lowercase)")
46
+ .option(
47
+ "--pack-type <type>",
48
+ `Pack boundary type (${PLUGIN_PACK_TYPES.join("|")})`,
49
+ "plugin"
50
+ )
51
+ .option(
52
+ "--stage <stage>",
53
+ `Load-order stage (${PLUGIN_LOAD_STAGES.join("|")})`,
54
+ "scan"
55
+ )
56
+ .option("--path <path>", "Destination file path override")
57
+ .option("--output-dir <path>", "Optional artifact output root override")
58
+ .option("--force", "Overwrite destination file if it already exists")
59
+ .option("--json", "Emit machine-readable output")
60
+ .action(async (options, command) => {
61
+ const pluginId = normalizePluginId(options.id);
62
+ const packType = normalizePluginPackType(options.packType);
63
+ const stage = normalizePluginLoadStage(options.stage);
64
+ const defaultPath = await resolveDefaultPluginManifestPath({
65
+ cwd: process.cwd(),
66
+ outputDir: options.outputDir,
67
+ env: process.env,
68
+ pluginId,
69
+ });
70
+ const outputPath = normalizeOutputPath(options.path, defaultPath);
71
+ const template = buildPluginManifestTemplate({
72
+ pluginId,
73
+ packType,
74
+ stage,
75
+ });
76
+ const manifest = validatePluginManifest(template);
77
+ const writtenPath = await writeJsonFile(outputPath, manifest, { force: Boolean(options.force) });
78
+
79
+ const payload = {
80
+ command: "plugin init",
81
+ pluginId,
82
+ packType: manifest.pack_type,
83
+ stage: manifest.load_order.stage,
84
+ outputPath: writtenPath,
85
+ schemaVersion: manifest.schema_version,
86
+ kind: manifest.kind,
87
+ };
88
+ if (shouldEmitJson(options, command)) {
89
+ console.log(JSON.stringify(payload, null, 2));
90
+ return;
91
+ }
92
+
93
+ console.log(pc.green(`Wrote plugin manifest: ${writtenPath}`));
94
+ console.log(pc.gray(`Plugin id: ${pluginId}`));
95
+ console.log(pc.gray(`Pack type: ${manifest.pack_type}`));
96
+ console.log(pc.gray(`Stage: ${manifest.load_order.stage}`));
97
+ console.log(pc.gray("Next: edit capabilities/security/load_order and validate before use."));
98
+ });
99
+
100
+ plugin
101
+ .command("validate")
102
+ .description("Validate a plugin manifest against Sentinelayer contract")
103
+ .requiredOption("--file <path>", "Manifest file to validate")
104
+ .option("--json", "Emit machine-readable output")
105
+ .action(async (options, command) => {
106
+ const inputPath = path.resolve(process.cwd(), String(options.file || "").trim());
107
+ const loaded = await readJsonFile(inputPath);
108
+ try {
109
+ const manifest = validatePluginManifest(loaded.data);
110
+ const payload = {
111
+ command: "plugin validate",
112
+ valid: true,
113
+ filePath: loaded.path,
114
+ pluginId: manifest.id,
115
+ version: manifest.version,
116
+ packType: manifest.pack_type,
117
+ stage: manifest.load_order.stage,
118
+ };
119
+ if (shouldEmitJson(options, command)) {
120
+ console.log(JSON.stringify(payload, null, 2));
121
+ return;
122
+ }
123
+
124
+ console.log(pc.green(`Manifest valid (${manifest.id}@${manifest.version})`));
125
+ console.log(pc.gray(`File: ${loaded.path}`));
126
+ } catch (error) {
127
+ const payload = {
128
+ command: "plugin validate",
129
+ valid: false,
130
+ filePath: loaded.path,
131
+ error: summarizePluginValidationError(error),
132
+ };
133
+ if (shouldEmitJson(options, command)) {
134
+ console.log(JSON.stringify(payload, null, 2));
135
+ } else {
136
+ console.log(pc.red(`Manifest invalid: ${payload.error}`));
137
+ console.log(pc.gray(`File: ${loaded.path}`));
138
+ }
139
+ process.exitCode = 2;
140
+ }
141
+ });
142
+
143
+ plugin
144
+ .command("list")
145
+ .description("List plugin manifests discovered under local artifact root")
146
+ .option("--path <path>", "Workspace path for config resolution", ".")
147
+ .option("--output-dir <path>", "Optional artifact output root override")
148
+ .option("--json", "Emit machine-readable output")
149
+ .action(async (options, command) => {
150
+ const targetPath = path.resolve(process.cwd(), String(options.path || "."));
151
+ const listing = await listPluginManifests({
152
+ cwd: targetPath,
153
+ outputDir: options.outputDir,
154
+ env: process.env,
155
+ });
156
+
157
+ const payload = {
158
+ command: "plugin list",
159
+ pluginsRoot: listing.pluginsRoot,
160
+ pluginCount: listing.plugins.length,
161
+ invalidCount: listing.invalid.length,
162
+ plugins: listing.plugins,
163
+ invalid: listing.invalid,
164
+ };
165
+
166
+ if (shouldEmitJson(options, command)) {
167
+ console.log(JSON.stringify(payload, null, 2));
168
+ } else {
169
+ console.log(pc.bold("Plugin manifests"));
170
+ console.log(pc.gray(`Root: ${listing.pluginsRoot}`));
171
+ if (!listing.plugins.length && !listing.invalid.length) {
172
+ console.log(pc.gray("(no plugin manifests found)"));
173
+ return;
174
+ }
175
+
176
+ for (const pluginEntry of listing.plugins) {
177
+ console.log(
178
+ `${pluginEntry.id}@${pluginEntry.version} | type=${pluginEntry.packType} | stage=${pluginEntry.stage} | commands=${pluginEntry.commandCount} | templates=${pluginEntry.templateCount} | policies=${pluginEntry.policyCount}`
179
+ );
180
+ console.log(pc.gray(` ${pluginEntry.path}`));
181
+ }
182
+
183
+ for (const invalidEntry of listing.invalid) {
184
+ console.log(pc.yellow(`invalid manifest: ${invalidEntry.path}`));
185
+ console.log(pc.gray(` ${invalidEntry.error}`));
186
+ }
187
+ }
188
+
189
+ if (listing.invalid.length > 0) {
190
+ process.exitCode = 2;
191
+ }
192
+ });
193
+
194
+ plugin
195
+ .command("order")
196
+ .description("Resolve deterministic plugin load order by stage")
197
+ .option("--path <path>", "Workspace path for config resolution", ".")
198
+ .option(
199
+ "--stage <stage>",
200
+ `Optional single-stage filter (${PLUGIN_LOAD_STAGES.join("|")})`
201
+ )
202
+ .option("--output-dir <path>", "Optional artifact output root override")
203
+ .option("--json", "Emit machine-readable output")
204
+ .action(async (options, command) => {
205
+ const targetPath = path.resolve(process.cwd(), String(options.path || "."));
206
+ const stage = options.stage ? normalizePluginLoadStage(options.stage) : "";
207
+ const ordering = await computePluginLoadOrder({
208
+ cwd: targetPath,
209
+ outputDir: options.outputDir,
210
+ env: process.env,
211
+ stage,
212
+ });
213
+
214
+ const payload = {
215
+ command: "plugin order",
216
+ pluginsRoot: ordering.pluginsRoot,
217
+ invalidCount: ordering.invalidCount,
218
+ hasBlockingIssues: ordering.hasBlockingIssues,
219
+ stages: ordering.stages,
220
+ invalid: ordering.invalid,
221
+ };
222
+
223
+ if (shouldEmitJson(options, command)) {
224
+ console.log(JSON.stringify(payload, null, 2));
225
+ } else {
226
+ console.log(pc.bold("Plugin load order"));
227
+ console.log(pc.gray(`Root: ${ordering.pluginsRoot}`));
228
+ for (const stageEntry of ordering.stages) {
229
+ console.log(
230
+ `${stageEntry.stage}: plugins=${stageEntry.pluginCount} cycle=${stageEntry.cycleDetected ? "yes" : "no"}`
231
+ );
232
+ if (stageEntry.order.length > 0) {
233
+ console.log(pc.gray(` order: ${stageEntry.order.join(" -> ")}`));
234
+ }
235
+ if (stageEntry.unresolvedReferences.length > 0) {
236
+ for (const unresolved of stageEntry.unresolvedReferences) {
237
+ console.log(
238
+ pc.yellow(
239
+ ` unresolved ${unresolved.relation}: ${unresolved.pluginId} -> ${unresolved.dependencyId}`
240
+ )
241
+ );
242
+ }
243
+ }
244
+ if (stageEntry.cycleNodes.length > 0) {
245
+ console.log(pc.red(` cycle nodes: ${stageEntry.cycleNodes.join(", ")}`));
246
+ }
247
+ }
248
+ if (ordering.invalid.length > 0) {
249
+ for (const invalidEntry of ordering.invalid) {
250
+ console.log(pc.yellow(`invalid manifest: ${invalidEntry.path}`));
251
+ console.log(pc.gray(` ${invalidEntry.error}`));
252
+ }
253
+ }
254
+ }
255
+
256
+ if (ordering.hasBlockingIssues) {
257
+ process.exitCode = 2;
258
+ }
259
+ });
260
+ }
@@ -0,0 +1,132 @@
1
+ import path from "node:path";
2
+ import process from "node:process";
3
+
4
+ import pc from "picocolors";
5
+
6
+ import { setConfigValue } from "../config/service.js";
7
+ import {
8
+ DEFAULT_POLICY_PACK_ID,
9
+ resolveActivePolicyPack,
10
+ resolvePolicyPackById,
11
+ } from "../policy/packs.js";
12
+
13
+ function shouldEmitJson(options, command) {
14
+ const local = Boolean(options && options.json);
15
+ const globalFromCommand =
16
+ command && command.optsWithGlobals ? Boolean(command.optsWithGlobals().json) : false;
17
+ return local || globalFromCommand;
18
+ }
19
+
20
+ function normalizeScope(rawValue) {
21
+ const normalized = String(rawValue || "project").trim().toLowerCase();
22
+ if (normalized !== "project" && normalized !== "global") {
23
+ throw new Error("scope must be project or global.");
24
+ }
25
+ return normalized;
26
+ }
27
+
28
+ export function registerPolicyCommand(program) {
29
+ const policy = program.command("policy").description("Manage Sentinelayer policy packs");
30
+
31
+ policy
32
+ .command("list")
33
+ .description("List built-in and plugin-provided policy packs")
34
+ .option("--path <path>", "Workspace path for config/plugin resolution", ".")
35
+ .option("--output-dir <path>", "Optional artifact output root override")
36
+ .option("--json", "Emit machine-readable output")
37
+ .action(async (options, command) => {
38
+ const targetPath = path.resolve(process.cwd(), String(options.path || "."));
39
+ const active = await resolveActivePolicyPack({
40
+ cwd: targetPath,
41
+ outputDir: options.outputDir,
42
+ env: process.env,
43
+ });
44
+
45
+ const payload = {
46
+ command: "policy list",
47
+ defaultPolicyPack: DEFAULT_POLICY_PACK_ID,
48
+ configuredPolicyPack: active.configuredId,
49
+ activePolicyPack: active.selected ? active.selected.id : null,
50
+ invalidManifestCount: active.listing.invalidManifestCount,
51
+ pluginRoot: active.listing.pluginsRoot,
52
+ packs: active.listing.packs.map((pack) => ({
53
+ id: pack.id,
54
+ name: pack.name,
55
+ source: pack.source,
56
+ description: pack.description,
57
+ scanProfile: pack.scanProfile,
58
+ plugin: pack.plugin || null,
59
+ })),
60
+ };
61
+
62
+ if (shouldEmitJson(options, command)) {
63
+ console.log(JSON.stringify(payload, null, 2));
64
+ return;
65
+ }
66
+
67
+ console.log(pc.bold("Policy packs"));
68
+ console.log(pc.gray(`Active: ${payload.activePolicyPack || "(none)"}`));
69
+ for (const pack of payload.packs) {
70
+ const marker = pack.id === payload.activePolicyPack ? "*" : " ";
71
+ const source = pack.source === "plugin" ? "plugin" : "builtin";
72
+ console.log(`${marker} ${pack.id} (${source}) - ${pack.description}`);
73
+ }
74
+ if (payload.invalidManifestCount > 0) {
75
+ console.log(
76
+ pc.yellow(
77
+ `Detected ${payload.invalidManifestCount} invalid plugin manifest(s); run 'plugin list --json' for details.`
78
+ )
79
+ );
80
+ }
81
+ });
82
+
83
+ policy
84
+ .command("use <packId>")
85
+ .description("Set active policy pack in config (project/global scope)")
86
+ .option("--path <path>", "Workspace path for config/plugin resolution", ".")
87
+ .option("--output-dir <path>", "Optional artifact output root override")
88
+ .option("--scope <scope>", "Write scope (project|global)", "project")
89
+ .option("--json", "Emit machine-readable output")
90
+ .action(async (packId, options, command) => {
91
+ const targetPath = path.resolve(process.cwd(), String(options.path || "."));
92
+ const scope = normalizeScope(options.scope);
93
+ const resolution = await resolvePolicyPackById({
94
+ packId,
95
+ cwd: targetPath,
96
+ outputDir: options.outputDir,
97
+ env: process.env,
98
+ });
99
+
100
+ if (!resolution.selected) {
101
+ const available = resolution.packs.map((pack) => pack.id).sort((left, right) => left.localeCompare(right));
102
+ throw new Error(
103
+ `Unknown policy pack '${resolution.packId}'. Available: ${available.join(", ") || "(none)"}`
104
+ );
105
+ }
106
+
107
+ const writeResult = await setConfigValue({
108
+ key: "defaultPolicyPack",
109
+ value: resolution.selected.id,
110
+ scope,
111
+ cwd: targetPath,
112
+ });
113
+
114
+ const payload = {
115
+ command: "policy use",
116
+ selected: resolution.selected.id,
117
+ source: resolution.selected.source,
118
+ scope: writeResult.scope,
119
+ configPath: writeResult.path,
120
+ scanProfile: resolution.selected.scanProfile,
121
+ };
122
+
123
+ if (shouldEmitJson(options, command)) {
124
+ console.log(JSON.stringify(payload, null, 2));
125
+ return;
126
+ }
127
+
128
+ console.log(pc.green(`Policy pack set to '${resolution.selected.id}' (${resolution.selected.source}).`));
129
+ console.log(pc.gray(`Scope: ${writeResult.scope}`));
130
+ console.log(pc.gray(`Config: ${writeResult.path}`));
131
+ });
132
+ }
@@ -0,0 +1,238 @@
1
+ import fs from "node:fs";
2
+ import fsp from "node:fs/promises";
3
+ import path from "node:path";
4
+
5
+ import pc from "picocolors";
6
+
7
+ import { formatIngestResolutionNotice, resolveCodebaseIngest } from "../ingest/engine.js";
8
+ import {
9
+ defaultPromptFileName,
10
+ generateExecutionPrompt,
11
+ resolvePromptTarget,
12
+ SUPPORTED_PROMPT_TARGETS,
13
+ } from "../prompt/generator.js";
14
+ import { renderTerminalMarkdown } from "../ui/markdown.js";
15
+
16
+ function shouldEmitJson(options, command) {
17
+ const local = Boolean(options && options.json);
18
+ const globalFromCommand =
19
+ command && command.optsWithGlobals ? Boolean(command.optsWithGlobals().json) : false;
20
+ return local || globalFromCommand;
21
+ }
22
+
23
+ function resolveSpecPath(targetPath, explicitSpecFile) {
24
+ const explicit = String(explicitSpecFile || "").trim();
25
+ if (explicit) {
26
+ return path.resolve(targetPath, explicit);
27
+ }
28
+
29
+ const candidates = [path.join(targetPath, "SPEC.md"), path.join(targetPath, "docs", "spec.md")];
30
+ const found = candidates.find((candidate) => fs.existsSync(candidate));
31
+ if (!found) {
32
+ throw new Error("No spec file found. Provide --spec-file or generate SPEC.md first.");
33
+ }
34
+ return found;
35
+ }
36
+
37
+ async function buildPromptOutput({
38
+ targetPath,
39
+ specFile,
40
+ agent,
41
+ outputFile,
42
+ outputDir = "",
43
+ refresh = false,
44
+ }) {
45
+ const ingestResolution = await resolveCodebaseIngest({
46
+ rootPath: targetPath,
47
+ outputDir,
48
+ refresh,
49
+ });
50
+ const resolvedSpecPath = resolveSpecPath(targetPath, specFile);
51
+ const specMarkdown = await fsp.readFile(resolvedSpecPath, "utf-8");
52
+ const resolvedAgent = resolvePromptTarget(agent);
53
+ const promptMarkdown = generateExecutionPrompt({
54
+ specMarkdown,
55
+ target: resolvedAgent,
56
+ projectPath: targetPath,
57
+ });
58
+
59
+ const outputName = String(outputFile || "").trim() || defaultPromptFileName(resolvedAgent);
60
+ const outputPath = path.resolve(targetPath, outputName);
61
+
62
+ return {
63
+ agent: resolvedAgent,
64
+ specPath: resolvedSpecPath,
65
+ promptMarkdown,
66
+ outputPath,
67
+ ingestRefresh: {
68
+ outputPath: ingestResolution.outputPath,
69
+ refreshed: ingestResolution.refreshed,
70
+ stale: ingestResolution.stale,
71
+ reasons: ingestResolution.reasons,
72
+ refreshedBecause: ingestResolution.refreshedBecause,
73
+ lastCommitAt: ingestResolution.lastCommitAt,
74
+ contentHash: ingestResolution.fingerprint?.contentHash || "",
75
+ },
76
+ };
77
+ }
78
+
79
+ function resolvePromptArtifactPath({ targetPath, promptFile, agent }) {
80
+ const explicit = String(promptFile || "").trim();
81
+ if (explicit) {
82
+ return path.resolve(targetPath, explicit);
83
+ }
84
+
85
+ const resolvedAgent = resolvePromptTarget(agent);
86
+ const defaultPath = path.resolve(targetPath, defaultPromptFileName(resolvedAgent));
87
+ if (!fs.existsSync(defaultPath)) {
88
+ throw new Error(
89
+ `No prompt artifact found at ${defaultPath}. Generate one with 'prompt generate' or pass --file.`
90
+ );
91
+ }
92
+ return defaultPath;
93
+ }
94
+
95
+ export function registerPromptCommand(program) {
96
+ const prompt = program
97
+ .command("prompt")
98
+ .description("Generate agent execution prompts from SPEC content");
99
+
100
+ prompt
101
+ .command("generate")
102
+ .description("Generate prompt markdown file from spec")
103
+ .option("--path <path>", "Target workspace path", ".")
104
+ .option("--spec-file <path>", "Spec file path relative to --path")
105
+ .option("--agent <target>", `Prompt target (${SUPPORTED_PROMPT_TARGETS.join("|")})`, "generic")
106
+ .option("--output-file <path>", "Output prompt file path relative to --path")
107
+ .option("--output-dir <path>", "Optional output dir override for ingest cache")
108
+ .option("--refresh", "Refresh CODEBASE_INGEST before generating prompt")
109
+ .option("--json", "Emit machine-readable output")
110
+ .action(async (options, command) => {
111
+ const targetPath = path.resolve(process.cwd(), String(options.path || "."));
112
+ const result = await buildPromptOutput({
113
+ targetPath,
114
+ specFile: options.specFile,
115
+ agent: options.agent,
116
+ outputFile: options.outputFile,
117
+ outputDir: options.outputDir,
118
+ refresh: Boolean(options.refresh),
119
+ });
120
+
121
+ await fsp.mkdir(path.dirname(result.outputPath), { recursive: true });
122
+ await fsp.writeFile(result.outputPath, `${result.promptMarkdown.trimEnd()}\n`, "utf-8");
123
+
124
+ if (shouldEmitJson(options, command)) {
125
+ console.log(
126
+ JSON.stringify(
127
+ {
128
+ command: "prompt generate",
129
+ agent: result.agent,
130
+ specPath: result.specPath,
131
+ outputPath: result.outputPath,
132
+ ingestRefresh: result.ingestRefresh,
133
+ },
134
+ null,
135
+ 2
136
+ )
137
+ );
138
+ return;
139
+ }
140
+
141
+ console.log(pc.bold("Prompt generated"));
142
+ console.log(pc.gray(`Agent: ${result.agent}`));
143
+ console.log(pc.gray(`Spec: ${result.specPath}`));
144
+ console.log(pc.gray(`Output: ${result.outputPath}`));
145
+ if (result.ingestRefresh?.stale || result.ingestRefresh?.refreshed) {
146
+ const color =
147
+ result.ingestRefresh?.stale && !result.ingestRefresh?.refreshed ? pc.yellow : pc.gray;
148
+ console.log(color(formatIngestResolutionNotice(result.ingestRefresh)));
149
+ }
150
+ });
151
+
152
+ prompt
153
+ .command("preview")
154
+ .description("Render generated prompt in terminal without writing file")
155
+ .option("--path <path>", "Target workspace path", ".")
156
+ .option("--spec-file <path>", "Spec file path relative to --path")
157
+ .option("--agent <target>", `Prompt target (${SUPPORTED_PROMPT_TARGETS.join("|")})`, "generic")
158
+ .option("--max-lines <n>", "Maximum lines to print (0 = unlimited)", "0")
159
+ .option("--output-dir <path>", "Optional output dir override for ingest cache")
160
+ .option("--refresh", "Refresh CODEBASE_INGEST before preview")
161
+ .option("--plain", "Disable terminal markdown styling")
162
+ .option("--json", "Emit machine-readable output")
163
+ .action(async (options, command) => {
164
+ const targetPath = path.resolve(process.cwd(), String(options.path || "."));
165
+ const result = await buildPromptOutput({
166
+ targetPath,
167
+ specFile: options.specFile,
168
+ agent: options.agent,
169
+ outputDir: options.outputDir,
170
+ refresh: Boolean(options.refresh),
171
+ });
172
+
173
+ const maxLines = Number.parseInt(String(options.maxLines || "0"), 10);
174
+ const lines = result.promptMarkdown.split(/\r?\n/);
175
+ const outputLines = Number.isFinite(maxLines) && maxLines > 0 ? lines.slice(0, maxLines) : lines;
176
+
177
+ if (shouldEmitJson(options, command)) {
178
+ console.log(
179
+ JSON.stringify(
180
+ {
181
+ command: "prompt preview",
182
+ agent: result.agent,
183
+ specPath: result.specPath,
184
+ lineCount: outputLines.length,
185
+ preview: outputLines.join("\n"),
186
+ ingestRefresh: result.ingestRefresh,
187
+ },
188
+ null,
189
+ 2
190
+ )
191
+ );
192
+ return;
193
+ }
194
+
195
+ if (result.ingestRefresh?.stale || result.ingestRefresh?.refreshed) {
196
+ const color =
197
+ result.ingestRefresh?.stale && !result.ingestRefresh?.refreshed ? pc.yellow : pc.gray;
198
+ console.log(color(formatIngestResolutionNotice(result.ingestRefresh)));
199
+ }
200
+ console.log(renderTerminalMarkdown(outputLines.join("\n"), { plain: Boolean(options.plain) }));
201
+ });
202
+
203
+ prompt
204
+ .command("show")
205
+ .description("Render an existing prompt artifact in terminal markdown")
206
+ .option("--path <path>", "Target workspace path", ".")
207
+ .option("--file <path>", "Prompt file path relative to --path")
208
+ .option("--agent <target>", `Prompt target (${SUPPORTED_PROMPT_TARGETS.join("|")})`, "generic")
209
+ .option("--plain", "Disable terminal markdown styling")
210
+ .option("--json", "Emit machine-readable output")
211
+ .action(async (options, command) => {
212
+ const targetPath = path.resolve(process.cwd(), String(options.path || "."));
213
+ const promptPath = resolvePromptArtifactPath({
214
+ targetPath,
215
+ promptFile: options.file,
216
+ agent: options.agent,
217
+ });
218
+ const markdown = await fsp.readFile(promptPath, "utf-8");
219
+
220
+ if (shouldEmitJson(options, command)) {
221
+ console.log(
222
+ JSON.stringify(
223
+ {
224
+ command: "prompt show",
225
+ promptPath,
226
+ lineCount: markdown.split(/\r?\n/).length,
227
+ preview: markdown,
228
+ },
229
+ null,
230
+ 2
231
+ )
232
+ );
233
+ return;
234
+ }
235
+
236
+ console.log(renderTerminalMarkdown(markdown, { plain: Boolean(options.plain) }));
237
+ });
238
+ }