nexarch 0.11.0 → 0.11.1

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/README.md CHANGED
@@ -25,6 +25,26 @@ npx nexarch setup
25
25
  | `nexarch init-agent [--json] [--strict] [--agent-id <id>] [--redact-hostname]` | Run onboarding handshake and mandatory `agent` entity registration in graph |
26
26
  | `nexarch init-agent --bind-to-external-key <key> [--bind-relationship-type <code>]` | Optionally bind the agent node to an existing graph external key |
27
27
  | `nexarch init-agent` (default behavior) | Also upserts technology component entities (host, OS, Node.js runtime) and links them to the agent |
28
+ | `nexarch agent identify ...` | Submit provider/model/client identity metadata to complete agent profile enrichment |
29
+
30
+ ## Bootstrap flow after `init-agent`
31
+
32
+ `nexarch init-agent` writes a bootstrap file at:
33
+
34
+ ```sh
35
+ ~/.nexarch/agent-bootstrap.md
36
+ ```
37
+
38
+ That file contains a pre-filled `npx nexarch agent identify ...` command template.
39
+ The current workflow is:
40
+
41
+ 1. run `npx nexarch init-agent`
42
+ 2. open `~/.nexarch/agent-bootstrap.md`
43
+ 3. copy the generated `npx nexarch agent identify ...` template
44
+ 4. fill in the real provider/model/client details
45
+ 5. run the completed command
46
+
47
+ Do not assume an `npx nexarch agent prompt` command exists unless/until it is explicitly implemented and documented.
28
48
 
29
49
  ## Company context behavior
30
50
 
@@ -21,14 +21,14 @@ export async function appliedPolicies(args) {
21
21
  const asJson = parseFlag(args, "--json");
22
22
  const showMarkdown = parseFlag(args, "--markdown");
23
23
  if (parseFlag(args, "--help") || parseFlag(args, "-h")) {
24
- console.log(`
25
- Usage:
26
- nexarch applied-policies [--pack <packCode>] [--markdown] [--json]
27
-
28
- Options:
29
- --pack <code> Filter to a specific policy pack code
30
- --markdown Include full document markdown in human output
31
- --json Print JSON response (always includes markdown)
24
+ console.log(`
25
+ Usage:
26
+ nexarch applied-policies [--pack <packCode>] [--markdown] [--json]
27
+
28
+ Options:
29
+ --pack <code> Filter to a specific policy pack code
30
+ --markdown Include full document markdown in human output
31
+ --json Print JSON response (always includes markdown)
32
32
  `);
33
33
  return;
34
34
  }
@@ -40,49 +40,82 @@ export async function checkIn(args) {
40
40
  const agentKey = agentKeyArg ?? identity.agentKey;
41
41
  if (!agentKey) {
42
42
  if (asJson) {
43
- process.stdout.write(JSON.stringify({ commands: [], reason: "no-agent-key" }) + "\n");
43
+ process.stdout.write(JSON.stringify({ commands: [], draftApplications: [], reason: "no-agent-key" }) + "\n");
44
44
  }
45
45
  else {
46
46
  console.log("No agent key found. Pass --agent-key or run nexarch init-agent first.");
47
47
  }
48
48
  return;
49
49
  }
50
- const raw = await callMcpTool("nexarch_claim_command", {
51
- agentRef: agentKey,
52
- agentKey,
53
- companyId: creds.companyId,
54
- }, { companyId: creds.companyId });
50
+ const [raw, draftRaw] = await Promise.all([
51
+ callMcpTool("nexarch_claim_command", {
52
+ agentRef: agentKey,
53
+ agentKey,
54
+ companyId: creds.companyId,
55
+ }, { companyId: creds.companyId }),
56
+ callMcpTool("nexarch_list_entities", {
57
+ companyId: creds.companyId,
58
+ entityTypeCode: "application",
59
+ status: "draft",
60
+ }, { companyId: creds.companyId }),
61
+ ]);
55
62
  const result = parseToolText(raw);
63
+ const draftResult = parseToolText(draftRaw);
64
+ const draftApplications = draftResult.entities ?? [];
56
65
  if (asJson) {
57
- process.stdout.write(JSON.stringify(result) + "\n");
66
+ process.stdout.write(JSON.stringify({ ...result, draftApplications }) + "\n");
58
67
  return;
59
68
  }
60
69
  const commands = result.commands ?? [];
61
- if (commands.length === 0) {
70
+ if (commands.length === 0 && draftApplications.length === 0) {
62
71
  console.log("No pending application-target commands found.");
72
+ console.log("No applications in draft state.");
63
73
  return;
64
74
  }
65
- console.log("\nPending application-target commands (preview only; nothing claimed):");
66
- console.log(`Company scope is resolved server-side from companyId=${creds.companyId}.`);
67
- if (identity.companyId && identity.companyId !== creds.companyId) {
68
- console.log(`Note: local identity companyId ${identity.companyId} differs from current credentials companyId ${creds.companyId}.`);
75
+ if (commands.length > 0) {
76
+ console.log("\nPending application-target commands (preview only; nothing claimed):");
77
+ console.log(`Company scope is resolved server-side from companyId=${creds.companyId}.`);
78
+ if (identity.companyId && identity.companyId !== creds.companyId) {
79
+ console.log(`Note: local identity companyId ${identity.companyId} differs from current credentials companyId ${creds.companyId}.`);
80
+ }
81
+ for (const cmd of commands) {
82
+ const claimable = cmd.claimable ? "yes" : "no";
83
+ const reason = cmd.claimReason ?? (cmd.claimable ? "application_access_confirmed" : "application_not_found_for_company");
84
+ console.log(`\n- ID : ${cmd.id}`);
85
+ console.log(` Type : ${cmd.command_type}`);
86
+ console.log(` Target : ${cmd.target_entity_key ?? "(none)"}`);
87
+ console.log(` Priority : ${cmd.priority}`);
88
+ console.log(` Claimable : ${claimable} (${reason})`);
89
+ if (cmd.command_type_version != null)
90
+ console.log(` Type ver : ${cmd.command_type_version}`);
91
+ if (cmd.resolved_runtime_handler)
92
+ console.log(` Runtime : ${cmd.resolved_runtime_handler}`);
93
+ if (cmd.claimable) {
94
+ console.log(` Claim cmd : npx nexarch command-claim --id "${cmd.id}"`);
95
+ }
96
+ }
97
+ console.log("\nTo validate an application target is accessible in Nexarch before claiming:");
98
+ console.log(" npx nexarch policy-controls --entity <application:key> --json");
99
+ }
100
+ else {
101
+ console.log("No pending application-target commands found.");
69
102
  }
70
- for (const cmd of commands) {
71
- const claimable = cmd.claimable ? "yes" : "no";
72
- const reason = cmd.claimReason ?? (cmd.claimable ? "application_access_confirmed" : "application_not_found_for_company");
73
- console.log(`\n- ID : ${cmd.id}`);
74
- console.log(` Type : ${cmd.command_type}`);
75
- console.log(` Target : ${cmd.target_entity_key ?? "(none)"}`);
76
- console.log(` Priority : ${cmd.priority}`);
77
- console.log(` Claimable : ${claimable} (${reason})`);
78
- if (cmd.command_type_version != null)
79
- console.log(` Type ver : ${cmd.command_type_version}`);
80
- if (cmd.resolved_runtime_handler)
81
- console.log(` Runtime : ${cmd.resolved_runtime_handler}`);
82
- if (cmd.claimable) {
83
- console.log(` Claim cmd : npx nexarch command-claim --id "${cmd.id}"`);
103
+ if (draftApplications.length > 0) {
104
+ const count = draftApplications.length;
105
+ console.log(`\n--- Draft applications (${count}) ---`);
106
+ console.log(`${count === 1 ? "There is 1 application" : `There are ${count} applications`} in draft state.` +
107
+ " Ask the user if they would like to review and instantiate any of them.");
108
+ for (const app of draftApplications) {
109
+ const subtype = app.entitySubtypeCode ? ` [${app.entitySubtypeCode}]` : "";
110
+ const key = app.externalKey ? ` (${app.externalKey})` : "";
111
+ console.log(`\n Name : ${app.name}${subtype}${key}`);
112
+ if (app.description)
113
+ console.log(` Desc : ${app.description}`);
114
+ console.log(` ID : ${app.id}`);
115
+ console.log(` Instantiate: npx nexarch proposals start --id "${app.id}"`);
84
116
  }
117
+ console.log("\nTo scaffold a draft application into a local project:");
118
+ console.log(" npx nexarch proposals start --id <applicationId>");
119
+ console.log(" npx nexarch proposals start --id <applicationId> --dir <path> --non-interactive");
85
120
  }
86
- console.log("\nTo validate an application target is accessible in Nexarch before claiming:");
87
- console.log(" npx nexarch policy-controls --entity <application:key> --json");
88
121
  }
@@ -11,12 +11,12 @@ function parseToolText(result) {
11
11
  export async function governanceSummary(args) {
12
12
  const asJson = parseFlag(args, "--json");
13
13
  if (parseFlag(args, "--help") || parseFlag(args, "-h")) {
14
- console.log(`
15
- Usage:
16
- nexarch governance-summary [--json]
17
-
18
- Returns review queue counts, graph stats, and a per-application policy
19
- audit rollup (latest run status, pass/partial/fail counts).
14
+ console.log(`
15
+ Usage:
16
+ nexarch governance-summary [--json]
17
+
18
+ Returns review queue counts, graph stats, and a per-application policy
19
+ audit rollup (latest run status, pass/partial/fail counts).
20
20
  `);
21
21
  return;
22
22
  }
@@ -168,13 +168,65 @@ function readSvnRepositoryUrl(dir) {
168
168
  return null;
169
169
  }
170
170
  }
171
+ function isLocalOnlyUrl(value) {
172
+ if (!value)
173
+ return true;
174
+ try {
175
+ const u = new URL(value);
176
+ const host = u.hostname.toLowerCase();
177
+ if (host === "localhost" || host === "127.0.0.1" || host === "0.0.0.0" || host === "::1")
178
+ return true;
179
+ return false;
180
+ }
181
+ catch {
182
+ return true;
183
+ }
184
+ }
185
+ function readGitRemoteCandidates(dir) {
186
+ const remotes = (safeExec("git", ["remote"], dir) ?? "")
187
+ .split("\n")
188
+ .map((value) => value.trim())
189
+ .filter(Boolean);
190
+ const names = remotes.length > 0 ? remotes : ["origin"];
191
+ const candidates = [];
192
+ for (const name of names) {
193
+ const value = safeExec("git", ["remote", "get-url", name], dir)
194
+ ?? safeExec("git", ["config", "--get", `remote.${name}.url`], dir);
195
+ if (value)
196
+ candidates.push(value);
197
+ }
198
+ return Array.from(new Set(candidates));
199
+ }
200
+ function choosePreferredRepositoryRef(candidates) {
201
+ if (candidates.length === 0)
202
+ return null;
203
+ const scored = candidates.map((rawRef, index) => {
204
+ const url = normalizeRepoUrl(rawRef);
205
+ const provider = inferProvider(url ?? rawRef);
206
+ const hostedProvider = provider !== "unknown";
207
+ const localOnly = isLocalOnlyUrl(url);
208
+ const score = [
209
+ hostedProvider ? 1 : 0,
210
+ !localOnly && Boolean(url) ? 1 : 0,
211
+ index === 0 ? 1 : 0,
212
+ ];
213
+ return { rawRef, score };
214
+ });
215
+ scored.sort((a, b) => {
216
+ for (let i = 0; i < a.score.length; i += 1) {
217
+ if (a.score[i] !== b.score[i])
218
+ return b.score[i] - a.score[i];
219
+ }
220
+ return 0;
221
+ });
222
+ return scored[0]?.rawRef ?? null;
223
+ }
171
224
  function detectSourceRepository(dir) {
172
225
  let rawRef = null;
173
226
  let vcsType = "unknown";
174
- // Prefer git when present.
227
+ // Prefer git when present, and prefer hosted remotes over local-only URLs.
175
228
  if (existsSync(join(dir, ".git"))) {
176
- rawRef = safeExec("git", ["remote", "get-url", "origin"], dir)
177
- ?? safeExec("git", ["config", "--get", "remote.origin.url"], dir);
229
+ rawRef = choosePreferredRepositoryRef(readGitRemoteCandidates(dir));
178
230
  if (rawRef)
179
231
  vcsType = "git";
180
232
  }
@@ -20,15 +20,15 @@ function parseToolText(result) {
20
20
  export async function policyAuditResults(args) {
21
21
  const asJson = parseFlag(args, "--json");
22
22
  if (parseFlag(args, "--help") || parseFlag(args, "-h")) {
23
- console.log(`
24
- Usage:
25
- nexarch policy-audit-results --entity <applicationEntityRef> [--limit <1-10>] [--json]
26
-
27
- Options:
28
- --entity <key> Required application reference (e.g. application:my-service).
29
- Aliases: --entity-ref, --application-ref, --application-key
30
- --limit <n> Number of most recent runs to return (default 1, max 10)
31
- --json Print JSON response
23
+ console.log(`
24
+ Usage:
25
+ nexarch policy-audit-results --entity <applicationEntityRef> [--limit <1-10>] [--json]
26
+
27
+ Options:
28
+ --entity <key> Required application reference (e.g. application:my-service).
29
+ Aliases: --entity-ref, --application-ref, --application-key
30
+ --limit <n> Number of most recent runs to return (default 1, max 10)
31
+ --json Print JSON response
32
32
  `);
33
33
  return;
34
34
  }
@@ -0,0 +1,589 @@
1
+ import process from "process";
2
+ import * as readline from "node:readline/promises";
3
+ import { existsSync, mkdirSync, readdirSync, statSync, writeFileSync } from "node:fs";
4
+ import { dirname, join, resolve as resolvePath } from "node:path";
5
+ import { requireCredentials } from "../lib/credentials.js";
6
+ import { callMcpTool } from "../lib/mcp.js";
7
+ function parseFlag(args, flag) {
8
+ return args.includes(flag);
9
+ }
10
+ function parseOptionValue(args, option) {
11
+ const idx = args.indexOf(option);
12
+ if (idx === -1)
13
+ return null;
14
+ const value = args[idx + 1];
15
+ if (!value || value.startsWith("--"))
16
+ return null;
17
+ return value;
18
+ }
19
+ function parseToolText(result) {
20
+ const text = result.content?.[0]?.text ?? "{}";
21
+ return JSON.parse(text);
22
+ }
23
+ function slugify(value) {
24
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "application";
25
+ }
26
+ function ensureDir(path) {
27
+ mkdirSync(path, { recursive: true });
28
+ }
29
+ function isDirectoryEmpty(path) {
30
+ if (!existsSync(path))
31
+ return true;
32
+ const stat = statSync(path);
33
+ if (!stat.isDirectory())
34
+ return false;
35
+ return readdirSync(path).length === 0;
36
+ }
37
+ function isDirectoryPath(path) {
38
+ return existsSync(path) && statSync(path).isDirectory();
39
+ }
40
+ function trimText(value) {
41
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
42
+ }
43
+ function formatBulletList(values) {
44
+ if (values.length === 0)
45
+ return "- None recorded";
46
+ return values.map((value) => `- ${value}`).join("\n");
47
+ }
48
+ function safeJson(value) {
49
+ return JSON.stringify(value, null, 2);
50
+ }
51
+ async function promptChoice(proposals) {
52
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
53
+ try {
54
+ console.log("\nProposed applications:");
55
+ proposals.forEach((proposal, index) => {
56
+ console.log(` ${index + 1}) ${proposal.name}${proposal.entitySubtypeCode ? ` [${proposal.entitySubtypeCode}]` : ""}`);
57
+ if (proposal.description)
58
+ console.log(` ${proposal.description}`);
59
+ });
60
+ // eslint-disable-next-line no-constant-condition
61
+ while (true) {
62
+ const answer = (await rl.question("\nChoose a proposal to start: ")).trim();
63
+ const pick = Number(answer);
64
+ if (Number.isFinite(pick) && pick >= 1 && pick <= proposals.length)
65
+ return proposals[pick - 1];
66
+ console.log(`Please enter a number between 1 and ${proposals.length}.`);
67
+ }
68
+ }
69
+ finally {
70
+ rl.close();
71
+ }
72
+ }
73
+ async function confirm(promptText, defaultYes = true) {
74
+ if (!process.stdin.isTTY || !process.stdout.isTTY)
75
+ return defaultYes;
76
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
77
+ try {
78
+ const answer = (await rl.question(promptText)).trim().toLowerCase();
79
+ if (!answer)
80
+ return defaultYes;
81
+ return answer === "y" || answer === "yes";
82
+ }
83
+ finally {
84
+ rl.close();
85
+ }
86
+ }
87
+ async function promptValue(promptText, fallback) {
88
+ if (!process.stdin.isTTY || !process.stdout.isTTY)
89
+ return fallback;
90
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
91
+ try {
92
+ const answer = (await rl.question(promptText)).trim();
93
+ return answer || fallback;
94
+ }
95
+ finally {
96
+ rl.close();
97
+ }
98
+ }
99
+ function normalizeForMatch(value) {
100
+ return (value ?? "").trim().toLowerCase();
101
+ }
102
+ function inferStackTemplate(proposal) {
103
+ const subtype = normalizeForMatch(proposal.application.entitySubtypeCode);
104
+ const haystack = [
105
+ proposal.application.name,
106
+ proposal.application.description,
107
+ proposal.application.entitySubtypeCode,
108
+ ...proposal.technologyChoices.flatMap((tech) => [tech.name, tech.description, tech.entitySubtypeCode, tech.entityTypeCode]),
109
+ ].map(normalizeForMatch).join(" ");
110
+ if (haystack.includes("next") || haystack.includes("next.js") || subtype.includes("web") || subtype.includes("portal") || subtype.includes("frontend")) {
111
+ return {
112
+ key: "next-web",
113
+ label: "Next.js web application",
114
+ runtime: "Node.js + Next.js",
115
+ devCommand: "npm run dev",
116
+ buildCommand: "npm run build",
117
+ entrySummary: "App Router web app with a simple landing page and architecture notes route.",
118
+ nextSteps: [
119
+ "Replace the placeholder app route with the real user journeys.",
120
+ "Connect the recommended services and data sources from the proposal.",
121
+ "Add policy control checks into CI before first release.",
122
+ ],
123
+ };
124
+ }
125
+ if (haystack.includes("react")) {
126
+ return {
127
+ key: "react-web",
128
+ label: "React front-end application",
129
+ runtime: "Node.js + React",
130
+ devCommand: "npm run dev",
131
+ buildCommand: "npm run build",
132
+ entrySummary: "React single-page app starter with proposal notes embedded in the UI.",
133
+ nextSteps: [
134
+ "Replace the placeholder components with the real application screens.",
135
+ "Wire in API/data integrations from the selected technology stack.",
136
+ "Add UI and accessibility tests before first handover.",
137
+ ],
138
+ };
139
+ }
140
+ if (haystack.includes("python") || haystack.includes("fastapi") || haystack.includes("django") || haystack.includes("flask")) {
141
+ return {
142
+ key: "python-api",
143
+ label: "Python API service",
144
+ runtime: "Python + FastAPI-style service",
145
+ devCommand: "python -m app.main",
146
+ buildCommand: "python -m compileall app",
147
+ entrySummary: "Python service scaffold with health endpoint and proposal docs.",
148
+ nextSteps: [
149
+ "Replace the placeholder endpoint with the application API surface.",
150
+ "Add dependency management and environment configuration for the target platform.",
151
+ "Implement the policy controls as startup checks, logging, and tests.",
152
+ ],
153
+ };
154
+ }
155
+ if (haystack.includes("worker") || haystack.includes("queue") || haystack.includes("job") || subtype.includes("service")) {
156
+ return {
157
+ key: "worker-service",
158
+ label: "Background worker service",
159
+ runtime: "Node.js worker/service",
160
+ devCommand: "npm run dev",
161
+ buildCommand: "npm run build",
162
+ entrySummary: "Worker starter with job processor placeholder and operational runbook notes.",
163
+ nextSteps: [
164
+ "Replace the sample processor with the real workload handler.",
165
+ "Connect queue, scheduler, or event infrastructure from the chosen stack.",
166
+ "Add observability and retry controls before production use.",
167
+ ],
168
+ };
169
+ }
170
+ if (haystack.includes("api") || haystack.includes("express") || haystack.includes("nest") || haystack.includes("service")) {
171
+ return {
172
+ key: "node-api",
173
+ label: "Node.js API service",
174
+ runtime: "Node.js + TypeScript API",
175
+ devCommand: "npm run dev",
176
+ buildCommand: "npm run build",
177
+ entrySummary: "TypeScript API starter with health route and proposal-backed service notes.",
178
+ nextSteps: [
179
+ "Replace the sample route with the real API contract.",
180
+ "Add persistence and integration adapters based on the selected technologies.",
181
+ "Implement automated tests and policy evidence capture in CI.",
182
+ ],
183
+ };
184
+ }
185
+ return {
186
+ key: "generic-ts",
187
+ label: "Generic TypeScript application",
188
+ runtime: "TypeScript starter",
189
+ devCommand: "npm run dev",
190
+ buildCommand: "npm run build",
191
+ entrySummary: "Lightweight TypeScript starter with proposal context and implementation placeholders.",
192
+ nextSteps: [
193
+ "Replace the starter files with the real application structure.",
194
+ "Bring in the frameworks and services recommended in the proposal.",
195
+ "Add delivery tests and policy evidence as implementation starts.",
196
+ ],
197
+ };
198
+ }
199
+ function buildReadme(proposal, template) {
200
+ const technologies = proposal.technologyChoices.map((item) => item.name);
201
+ const policies = proposal.policyControls.map((item) => item.name);
202
+ return `# ${proposal.application.name}
203
+
204
+ ${proposal.application.description ?? "Application scaffold generated from a NexArch proposal."}
205
+
206
+ ## Starter template
207
+
208
+ - Template: ${template.label}
209
+ - Runtime: ${template.runtime}
210
+ - Suggested dev command: \`${template.devCommand}\`
211
+ - Suggested build command: \`${template.buildCommand}\`
212
+ - Starter shape: ${template.entrySummary}
213
+
214
+ ## Proposal summary
215
+
216
+ - Workflow state: ${proposal.application.workflowState}
217
+ - Application subtype: ${proposal.application.entitySubtypeCode ?? "Not specified"}
218
+ - Recommendation basis: ${proposal.application.recommendationBasis ?? "Not specified"}
219
+ - NexArch reference: ${proposal.application.entityRef ?? proposal.application.id}
220
+
221
+ ${proposal.application.recommendationSummary ? `## Recommendation notes\n\n${proposal.application.recommendationSummary}\n\n` : ""}## Suggested technology choices
222
+
223
+ ${formatBulletList(technologies)}
224
+
225
+ ## Policy controls to satisfy
226
+
227
+ ${formatBulletList(policies)}
228
+
229
+ ## Getting started
230
+
231
+ ${template.nextSteps.map((step, index) => `${index + 1}. ${step}`).join("\n")}
232
+ `;
233
+ }
234
+ function buildProposalMarkdown(proposal) {
235
+ const techLines = proposal.technologyChoices.map((tech) => `${tech.name}${tech.entitySubtypeCode ? ` (${tech.entitySubtypeCode})` : ""}${tech.description ? ` — ${tech.description}` : ""}`);
236
+ const policyLines = proposal.policyControls.map((control) => `${control.name}${control.description ? ` — ${control.description}` : ""}`);
237
+ return `# NexArch proposal context\n\n## Application\n\n- Name: ${proposal.application.name}\n- ID: ${proposal.application.id}\n- Reference: ${proposal.application.entityRef ?? "n/a"}\n- Subtype: ${proposal.application.entitySubtypeCode ?? "n/a"}\n- Description: ${proposal.application.description ?? "n/a"}\n- Recommendation basis: ${proposal.application.recommendationBasis ?? "n/a"}\n- Proposal source: ${proposal.proposal.proposalSource ?? "n/a"}\n\n${proposal.application.recommendationSummary ? `## Recommendation summary\n\n${proposal.application.recommendationSummary}\n\n` : ""}## Technology choices\n\n${formatBulletList(techLines)}\n\n## Policy controls\n\n${formatBulletList(policyLines)}\n`;
238
+ }
239
+ function buildPolicyMarkdown(proposal) {
240
+ if (proposal.policyControls.length === 0) {
241
+ return "# Policy controls\n\nNo explicit policy controls were linked to this proposal.\n";
242
+ }
243
+ return `# Policy controls\n\n${proposal.policyControls.map((control) => {
244
+ const guidance = trimText(typeof control.controlAttributes.summary === "string" ? control.controlAttributes.summary : null);
245
+ const rules = Array.isArray(control.controlAttributes.rules)
246
+ ? control.controlAttributes.rules
247
+ .map((rule) => trimText(typeof rule.title === "string" ? rule.title : typeof rule.code === "string" ? rule.code : null))
248
+ .filter((value) => Boolean(value))
249
+ : [];
250
+ return `## ${control.name}\n\n${control.description ?? "No description provided."}\n\n${guidance ? `Guidance: ${guidance}\n\n` : ""}${rules.length > 0 ? `Rules\n\n${formatBulletList(rules)}\n` : ""}`;
251
+ }).join("\n")}`;
252
+ }
253
+ function buildTsConfig() {
254
+ return `{
255
+ "compilerOptions": {
256
+ "target": "ES2022",
257
+ "module": "NodeNext",
258
+ "moduleResolution": "NodeNext",
259
+ "strict": true,
260
+ "esModuleInterop": true,
261
+ "forceConsistentCasingInFileNames": true,
262
+ "skipLibCheck": true,
263
+ "outDir": "dist"
264
+ },
265
+ "include": ["src/**/*.ts"]
266
+ }
267
+ `;
268
+ }
269
+ function buildPackageJson(proposal, template) {
270
+ const base = {
271
+ name: slugify(proposal.application.name),
272
+ version: "0.1.0",
273
+ private: true,
274
+ description: proposal.application.description ?? `Scaffold for ${proposal.application.name}`,
275
+ };
276
+ if (template.key === "next-web") {
277
+ return `${safeJson({
278
+ ...base,
279
+ scripts: { dev: "next dev", build: "next build", start: "next start", lint: "next lint" },
280
+ dependencies: { next: "latest", react: "latest", "react-dom": "latest" },
281
+ })}\n`;
282
+ }
283
+ if (template.key === "react-web") {
284
+ return `${safeJson({
285
+ ...base,
286
+ scripts: { dev: "vite", build: "vite build", preview: "vite preview" },
287
+ dependencies: { react: "latest", "react-dom": "latest" },
288
+ devDependencies: { typescript: "latest", vite: "latest" },
289
+ })}\n`;
290
+ }
291
+ if (template.key === "python-api") {
292
+ return `${safeJson({
293
+ ...base,
294
+ note: "Python scaffold - see pyproject.toml for runtime dependencies.",
295
+ })}\n`;
296
+ }
297
+ return `${safeJson({
298
+ ...base,
299
+ type: "module",
300
+ scripts: { dev: "node --watch src/index.ts", build: "tsc --noEmit false", start: "node dist/index.js" },
301
+ devDependencies: { typescript: "latest", "@types/node": "latest" },
302
+ })}\n`;
303
+ }
304
+ function buildStarterSource(proposal, template) {
305
+ const suffix = proposal.application.name.replace(/[^a-zA-Z0-9]+/g, "") || "Application";
306
+ switch (template.key) {
307
+ case "next-web":
308
+ return [
309
+ {
310
+ relativePath: "src/app/page.tsx",
311
+ content: `export default function Home() {
312
+ return (
313
+ <main style={{ padding: 32, fontFamily: "Arial, sans-serif" }}>
314
+ <h1>${proposal.application.name}</h1>
315
+ <p>${proposal.application.description ?? "Application scaffold generated from a NexArch proposal."}</p>
316
+ <p>Start here by replacing this page with the real user journey.</p>
317
+ </main>
318
+ );
319
+ }
320
+ `,
321
+ },
322
+ {
323
+ relativePath: "src/app/layout.tsx",
324
+ content: `export default function RootLayout({ children }: { children: React.ReactNode }) {
325
+ return (
326
+ <html lang="en">
327
+ <body>{children}</body>
328
+ </html>
329
+ );
330
+ }
331
+ `,
332
+ },
333
+ ];
334
+ case "react-web":
335
+ return [
336
+ {
337
+ relativePath: "src/main.tsx",
338
+ content: `import React from "react";
339
+ import ReactDOM from "react-dom/client";
340
+ import { App } from "./App";
341
+
342
+ ReactDOM.createRoot(document.getElementById("root")!).render(
343
+ <React.StrictMode>
344
+ <App />
345
+ </React.StrictMode>,
346
+ );
347
+ `,
348
+ },
349
+ {
350
+ relativePath: "src/App.tsx",
351
+ content: `export function App() {
352
+ return (
353
+ <main style={{ padding: 32, fontFamily: "Arial, sans-serif" }}>
354
+ <h1>${proposal.application.name}</h1>
355
+ <p>${proposal.application.description ?? "Application scaffold generated from a NexArch proposal."}</p>
356
+ </main>
357
+ );
358
+ }
359
+ `,
360
+ },
361
+ {
362
+ relativePath: "index.html",
363
+ content: `<!doctype html>
364
+ <html lang="en">
365
+ <head>
366
+ <meta charset="UTF-8" />
367
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
368
+ <title>${proposal.application.name}</title>
369
+ </head>
370
+ <body>
371
+ <div id="root"></div>
372
+ <script type="module" src="/src/main.tsx"></script>
373
+ </body>
374
+ </html>
375
+ `,
376
+ },
377
+ ];
378
+ case "python-api":
379
+ return [
380
+ {
381
+ relativePath: "app/main.py",
382
+ content: `def healthcheck() -> dict[str, str]:
383
+ return {"status": "ok", "application": ${JSON.stringify(proposal.application.name)}}
384
+
385
+
386
+ if __name__ == "__main__":
387
+ print(healthcheck())
388
+ `,
389
+ },
390
+ {
391
+ relativePath: "pyproject.toml",
392
+ content: `[project]
393
+ name = "${slugify(proposal.application.name)}"
394
+ version = "0.1.0"
395
+ description = ${JSON.stringify(proposal.application.description ?? `Scaffold for ${proposal.application.name}`)}
396
+ requires-python = ">=3.11"
397
+ dependencies = []
398
+ `,
399
+ },
400
+ ];
401
+ case "worker-service":
402
+ return [
403
+ {
404
+ relativePath: "src/index.ts",
405
+ content: `export async function start${suffix}Worker(): Promise<void> {
406
+ console.log(${JSON.stringify(`Worker scaffold ready for ${proposal.application.name}`)});
407
+ }
408
+
409
+ start${suffix}Worker().catch((error) => {
410
+ console.error(error);
411
+ process.exitCode = 1;
412
+ });
413
+ `,
414
+ },
415
+ {
416
+ relativePath: "src/jobs/example-job.ts",
417
+ content: `export async function processExampleJob(): Promise<void> {
418
+ console.log("Replace this with the real job handler.");
419
+ }
420
+ `,
421
+ },
422
+ ];
423
+ case "node-api":
424
+ return [
425
+ {
426
+ relativePath: "src/index.ts",
427
+ content: `import { createServer } from "node:http";
428
+
429
+ const server = createServer((_req, res) => {
430
+ res.writeHead(200, { "content-type": "application/json" });
431
+ res.end(JSON.stringify({ status: "ok", application: ${JSON.stringify(proposal.application.name)} }));
432
+ });
433
+
434
+ server.listen(3000, () => {
435
+ console.log("API starter listening on http://localhost:3000");
436
+ });
437
+ `,
438
+ },
439
+ ];
440
+ default:
441
+ return [
442
+ {
443
+ relativePath: "src/index.ts",
444
+ content: `export function start${suffix}App(): void {
445
+ console.log(${JSON.stringify(`Start building ${proposal.application.name}`)});
446
+ }
447
+ `,
448
+ },
449
+ ];
450
+ }
451
+ }
452
+ function buildTemplateFiles(proposal, template) {
453
+ const shared = [
454
+ { relativePath: ".gitignore", content: "node_modules/\ndist/\n.env\n.DS_Store\n.venv/\n" },
455
+ { relativePath: ".editorconfig", content: "root = true\n\n[*]\ncharset = utf-8\nend_of_line = lf\ninsert_final_newline = true\nindent_style = space\nindent_size = 2\n" },
456
+ { relativePath: "README.md", content: buildReadme(proposal, template) },
457
+ { relativePath: "docs/architecture/nexarch-proposal.md", content: buildProposalMarkdown(proposal) },
458
+ { relativePath: "docs/architecture/policy-controls.md", content: buildPolicyMarkdown(proposal) },
459
+ { relativePath: ".nexarch/proposal.json", content: `${safeJson({ ...proposal, scaffoldTemplate: template })}\n` },
460
+ { relativePath: "tests/README.md", content: "Add delivery tests for the new application here.\n" },
461
+ { relativePath: ".env.example", content: "# Add environment variables for this application here\n" },
462
+ ];
463
+ shared.push({ relativePath: "package.json", content: buildPackageJson(proposal, template) });
464
+ if (["node-api", "worker-service", "generic-ts"].includes(template.key)) {
465
+ shared.push({ relativePath: "tsconfig.json", content: buildTsConfig() });
466
+ }
467
+ return [...shared, ...buildStarterSource(proposal, template)];
468
+ }
469
+ function scaffoldProposal(targetDir, proposal) {
470
+ const template = inferStackTemplate(proposal);
471
+ const files = buildTemplateFiles(proposal, template);
472
+ for (const file of files) {
473
+ const outputPath = join(targetDir, file.relativePath);
474
+ ensureDir(dirname(outputPath));
475
+ writeFileSync(outputPath, file.content, "utf8");
476
+ }
477
+ return {
478
+ createdFiles: files.map((file) => join(targetDir, file.relativePath)),
479
+ template,
480
+ };
481
+ }
482
+ export async function proposalsStart(args) {
483
+ const asJson = parseFlag(args, "--json");
484
+ const nonInteractive = parseFlag(args, "--non-interactive");
485
+ const force = parseFlag(args, "--force");
486
+ const skipActivate = parseFlag(args, "--skip-activate");
487
+ const activateFlag = parseFlag(args, "--activate");
488
+ const applicationId = parseOptionValue(args, "--id") ?? parseOptionValue(args, "--application-id");
489
+ const targetDirArg = parseOptionValue(args, "--dir");
490
+ const reason = parseOptionValue(args, "--reason") ?? "Application scaffold started from approved proposal workflow";
491
+ const repoUrl = parseOptionValue(args, "--repo");
492
+ const creds = requireCredentials();
493
+ const listRaw = await callMcpTool("nexarch_list_proposed_applications", { companyId: creds.companyId }, { companyId: creds.companyId });
494
+ const list = parseToolText(listRaw);
495
+ const proposals = list.proposals ?? [];
496
+ if (proposals.length === 0) {
497
+ const payload = { ok: true, proposals: [], message: "No proposed applications found." };
498
+ if (asJson)
499
+ process.stdout.write(`${JSON.stringify(payload)}\n`);
500
+ else
501
+ console.log(payload.message);
502
+ return;
503
+ }
504
+ let chosen = applicationId
505
+ ? proposals.find((proposal) => proposal.id === applicationId)
506
+ : undefined;
507
+ if (!chosen && applicationId) {
508
+ throw new Error(`Proposal not found: ${applicationId}`);
509
+ }
510
+ if (!chosen) {
511
+ if (nonInteractive || !process.stdin.isTTY || !process.stdout.isTTY) {
512
+ if (proposals.length === 1) {
513
+ chosen = proposals[0];
514
+ }
515
+ else {
516
+ throw new Error("Multiple proposed applications found. Re-run with --id <applicationId>.");
517
+ }
518
+ }
519
+ else {
520
+ chosen = await promptChoice(proposals);
521
+ }
522
+ }
523
+ const getRaw = await callMcpTool("nexarch_get_proposed_application", { applicationId: chosen.id, companyId: creds.companyId }, { companyId: creds.companyId });
524
+ const getResult = parseToolText(getRaw);
525
+ const proposal = getResult.proposal;
526
+ if (!proposal)
527
+ throw new Error("Proposal context was not returned by MCP gateway.");
528
+ const suggestedDir = resolvePath(targetDirArg ?? slugify(proposal.application.name));
529
+ const targetDir = nonInteractive || targetDirArg
530
+ ? suggestedDir
531
+ : resolvePath(await promptValue(`Target directory [${suggestedDir}]: `, suggestedDir));
532
+ if (existsSync(targetDir) && !isDirectoryPath(targetDir)) {
533
+ throw new Error(`Target path is not a directory: ${targetDir}`);
534
+ }
535
+ if (existsSync(targetDir) && !isDirectoryEmpty(targetDir) && !force) {
536
+ throw new Error(`Target directory is not empty: ${targetDir}. Re-run with --force to allow writing into it.`);
537
+ }
538
+ const scaffold = scaffoldProposal(targetDir, proposal);
539
+ let activated = null;
540
+ const shouldActivate = skipActivate
541
+ ? false
542
+ : activateFlag || nonInteractive || !process.stdin.isTTY || !process.stdout.isTTY
543
+ ? true
544
+ : await confirm("Activate this proposal in NexArch now? [Y/n]: ", true);
545
+ if (shouldActivate) {
546
+ const activateRaw = await callMcpTool("nexarch_activate_proposed_application", {
547
+ applicationId: proposal.application.id,
548
+ companyId: creds.companyId,
549
+ reason,
550
+ scaffold: {
551
+ command: "nexarch proposals start",
552
+ template: scaffold.template,
553
+ targetDir,
554
+ repoUrl: trimText(repoUrl),
555
+ createdAt: new Date().toISOString(),
556
+ createdFiles: scaffold.createdFiles,
557
+ },
558
+ agentContext: {
559
+ initiatedBy: "nexarch-cli",
560
+ command: "proposals start",
561
+ operatorEmail: creds.email,
562
+ },
563
+ }, { companyId: creds.companyId });
564
+ activated = parseToolText(activateRaw);
565
+ }
566
+ const payload = {
567
+ ok: true,
568
+ proposalId: proposal.application.id,
569
+ proposalName: proposal.application.name,
570
+ targetDir,
571
+ activated,
572
+ scaffold,
573
+ };
574
+ if (asJson) {
575
+ process.stdout.write(`${JSON.stringify(payload)}\n`);
576
+ return;
577
+ }
578
+ console.log(`\nStarted ${proposal.application.name}`);
579
+ console.log(`- Proposal: ${proposal.application.id}`);
580
+ console.log(`- Folder : ${targetDir}`);
581
+ console.log(`- Template: ${scaffold.template.label}`);
582
+ console.log(`- Runtime : ${scaffold.template.runtime}`);
583
+ console.log(`- Files : ${scaffold.createdFiles.length} created`);
584
+ console.log(`- State : ${activated ? `${activated.status}${activated.alreadyActive ? " (already active)" : ""}` : "left in proposed state"}`);
585
+ console.log("\nNext steps:");
586
+ scaffold.template.nextSteps.forEach((step, index) => console.log(`${index + 1}. ${step}`));
587
+ console.log(`${scaffold.template.nextSteps.length + 1}. Use docs/architecture/nexarch-proposal.md and policy-controls.md while building.`);
588
+ console.log(`${scaffold.template.nextSteps.length + 2}. Check the delivery back into NexArch when implementation is underway.`);
589
+ }
package/dist/index.js CHANGED
@@ -24,6 +24,7 @@ import { policyAuditSubmit } from "./commands/policy-audit-submit.js";
24
24
  import { policyAuditResults } from "./commands/policy-audit-results.js";
25
25
  import { appliedPolicies } from "./commands/applied-policies.js";
26
26
  import { governanceSummary } from "./commands/governance-summary.js";
27
+ import { proposalsStart } from "./commands/proposals-start.js";
27
28
  const [, , command, ...args] = process.argv;
28
29
  const commands = {
29
30
  login,
@@ -61,6 +62,13 @@ async function main() {
61
62
  return;
62
63
  }
63
64
  }
65
+ if (command === "proposals") {
66
+ const [subcommand, ...subArgs] = args;
67
+ if (subcommand === "start") {
68
+ await proposalsStart(subArgs);
69
+ return;
70
+ }
71
+ }
64
72
  const handler = commands[command ?? ""];
65
73
  if (!handler) {
66
74
  console.log(`
@@ -172,9 +180,24 @@ Usage:
172
180
  --to <toExternalKey>
173
181
  --limit <1-500>
174
182
  --json
175
- nexarch check-in Preview pending application-target commands (no auto-claim).
183
+ nexarch check-in Preview pending application-target commands (no auto-claim)
184
+ and report any applications currently in draft state so the
185
+ agent can prompt the user to explore and instantiate them.
176
186
  Scope is resolved server-side from active company context.
177
187
  Options: --agent-key <key> override stored agent key
188
+ --json JSON output includes draftApplications[]
189
+ nexarch proposals start
190
+ Start a new application workspace from a proposed NexArch app.
191
+ Lists proposed apps, lets you choose one, writes a starter
192
+ project scaffold, and activates the proposal to active.
193
+ Options: --id <applicationId>
194
+ --dir <path>
195
+ --reason <text>
196
+ --repo <url>
197
+ --skip-activate
198
+ --activate
199
+ --force
200
+ --non-interactive
178
201
  --json
179
202
  nexarch command-claim
180
203
  Explicitly claim a pending command by ID.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexarch",
3
- "version": "0.11.0",
3
+ "version": "0.11.1",
4
4
  "description": "Your architecture workspace for AI delivery.",
5
5
  "keywords": [
6
6
  "nexarch",