nexarch 0.11.4 → 0.12.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.
@@ -2,6 +2,7 @@ import process from "process";
2
2
  import { existsSync, readFileSync } from "fs";
3
3
  import { join } from "path";
4
4
  import { homedir } from "os";
5
+ import { saveCheckInLease } from "../lib/check-in-lease.js";
5
6
  import { requireCredentials } from "../lib/credentials.js";
6
7
  import { callMcpTool } from "../lib/mcp.js";
7
8
  function parseFlag(args, flag) {
@@ -55,11 +56,28 @@ export async function checkIn(args) {
55
56
  const result = parseToolText(raw);
56
57
  const draftApplications = result.draftApplications ?? [];
57
58
  const proposedApplications = result.proposedApplications ?? [];
59
+ if (result.leaseToken && result.leaseIssuedAt && result.leaseExpiresAt) {
60
+ saveCheckInLease({
61
+ leaseToken: result.leaseToken,
62
+ leaseIssuedAt: result.leaseIssuedAt,
63
+ leaseExpiresAt: result.leaseExpiresAt,
64
+ policyBundleHash: result.policyBundleHash ?? null,
65
+ companyId: creds.companyId,
66
+ agentRef: agentKey,
67
+ });
68
+ }
58
69
  if (asJson) {
59
70
  process.stdout.write(JSON.stringify(result) + "\n");
60
71
  return;
61
72
  }
62
73
  const commands = result.commands ?? [];
74
+ if (result.leaseExpiresAt) {
75
+ console.log(`Check-in lease active until ${new Date(result.leaseExpiresAt).toLocaleString()}.`);
76
+ }
77
+ for (const warning of result.warnings ?? []) {
78
+ if (warning.message)
79
+ console.log(`Warning: ${warning.message}`);
80
+ }
63
81
  if (commands.length === 0 && draftApplications.length === 0 && proposedApplications.length === 0) {
64
82
  console.log("No pending application-target commands found.");
65
83
  console.log("No draft or proposed applications need attention.");
@@ -124,8 +142,12 @@ export async function checkIn(args) {
124
142
  console.log(` Desc : ${app.description}`);
125
143
  if (app.recommendationBasis)
126
144
  console.log(` Basis : ${app.recommendationBasis}`);
145
+ if (app.recommendationStatus)
146
+ console.log(` Status : ${app.recommendationStatus}`);
127
147
  if (typeof app.recommendedTechnologyCount === "number")
128
148
  console.log(` Tech : ${app.recommendedTechnologyCount} recommended`);
149
+ if (typeof app.capabilityGapCount === "number")
150
+ console.log(` Gaps : ${app.capabilityGapCount} unresolved`);
129
151
  if (typeof app.confirmedPolicyCount === "number")
130
152
  console.log(` Policy : ${app.confirmedPolicyCount} confirmed`);
131
153
  if (app.requiresPolicyReview)
@@ -2,6 +2,7 @@ import process from "process";
2
2
  import { existsSync, readFileSync } from "fs";
3
3
  import { join } from "path";
4
4
  import { homedir } from "os";
5
+ import { selectCheckInLease } from "../lib/check-in-lease.js";
5
6
  import { requireCredentials } from "../lib/credentials.js";
6
7
  import { callMcpTool } from "../lib/mcp.js";
7
8
  function parseFlag(args, flag) {
@@ -47,10 +48,12 @@ export async function commandClaim(args) {
47
48
  console.error("error: no agent key found. Pass --agent-key or run nexarch init-agent first.");
48
49
  process.exit(1);
49
50
  }
51
+ const lease = selectCheckInLease(creds.companyId, agentKey);
50
52
  const raw = await callMcpTool("nexarch_claim_command_by_id", {
51
53
  commandId: id,
52
54
  agentRef: agentKey,
53
55
  agentKey,
56
+ leaseToken: lease?.leaseToken,
54
57
  companyId: creds.companyId,
55
58
  }, { companyId: creds.companyId });
56
59
  const result = parseToolText(raw);
@@ -58,6 +61,13 @@ export async function commandClaim(args) {
58
61
  process.stdout.write(JSON.stringify(result) + "\n");
59
62
  return;
60
63
  }
64
+ if ((result.warnings ?? []).length > 0) {
65
+ for (const warning of result.warnings ?? []) {
66
+ if (warning.message)
67
+ console.log(`Warning: ${warning.message}`);
68
+ }
69
+ console.log("Tip: run `npx nexarch check-in` to refresh your lease before delivery actions.");
70
+ }
61
71
  if (!result.command) {
62
72
  console.log(`Command ${id} was not claimed.`);
63
73
  if (result.reason)
@@ -1,7 +1,9 @@
1
1
  import process from "process";
2
2
  import * as readline from "node:readline/promises";
3
- import { existsSync, mkdirSync, readdirSync, statSync, writeFileSync } from "node:fs";
3
+ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
4
+ import { homedir } from "node:os";
4
5
  import { dirname, join, resolve as resolvePath } from "node:path";
6
+ import { selectCheckInLease } from "../lib/check-in-lease.js";
5
7
  import { requireCredentials } from "../lib/credentials.js";
6
8
  import { callMcpTool } from "../lib/mcp.js";
7
9
  function parseFlag(args, flag) {
@@ -20,6 +22,18 @@ function parseToolText(result) {
20
22
  const text = result.content?.[0]?.text ?? "{}";
21
23
  return JSON.parse(text);
22
24
  }
25
+ function loadIdentity() {
26
+ const identityPath = join(homedir(), ".nexarch", "identity.json");
27
+ if (!existsSync(identityPath))
28
+ return { agentKey: null };
29
+ try {
30
+ const data = JSON.parse(readFileSync(identityPath, "utf8"));
31
+ return { agentKey: data.agentKey ?? null };
32
+ }
33
+ catch {
34
+ return { agentKey: null };
35
+ }
36
+ }
23
37
  function slugify(value) {
24
38
  return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "application";
25
39
  }
@@ -199,6 +213,8 @@ function inferStackTemplate(proposal) {
199
213
  function buildReadme(proposal, template) {
200
214
  const technologies = proposal.technologyChoices.map((item) => item.name);
201
215
  const policies = proposal.policyControls.map((item) => item.name);
216
+ const capabilityGaps = (proposal.proposal.recommendationCapabilityGaps ?? [])
217
+ .map((item) => `${item.capability ?? "Unspecified capability"}${item.layer ? ` (${item.layer})` : ""}${item.severity ? ` — ${item.severity}` : ""}${item.reason ? `: ${item.reason}` : ""}`);
202
218
  return `# ${proposal.application.name}
203
219
 
204
220
  ${proposal.application.description ?? "Application scaffold generated from a NexArch proposal."}
@@ -216,13 +232,14 @@ ${proposal.proposal.requiresPolicyReview ? `> Policy review is required before a
216
232
  - Workflow state: ${proposal.application.workflowState}
217
233
  - Application subtype: ${proposal.application.entitySubtypeCode ?? "Not specified"}
218
234
  - Recommendation basis: ${proposal.application.recommendationBasis ?? "Not specified"}
235
+ - Recommendation status: ${proposal.application.recommendationStatus ?? proposal.proposal.recommendationStatus ?? "Not specified"}
219
236
  - NexArch reference: ${proposal.application.entityRef ?? proposal.application.id}
220
237
 
221
238
  ${proposal.application.recommendationSummary ? `## Recommendation notes\n\n${proposal.application.recommendationSummary}\n\n` : ""}## Suggested technology choices
222
239
 
223
240
  ${formatBulletList(technologies)}
224
241
 
225
- ## Policy controls to satisfy
242
+ ${capabilityGaps.length > 0 ? `## Capability gaps\n\n${formatBulletList(capabilityGaps)}\n\n` : ""}## Policy controls to satisfy
226
243
 
227
244
  ${formatBulletList(policies)}
228
245
 
@@ -233,11 +250,20 @@ ${template.nextSteps.map((step, index) => `${index + 1}. ${step}`).join("\n")}
233
250
  }
234
251
  function buildProposalMarkdown(proposal) {
235
252
  const techLines = proposal.technologyChoices.map((tech) => `${tech.name}${tech.entitySubtypeCode ? ` (${tech.entitySubtypeCode})` : ""}${tech.description ? ` — ${tech.description}` : ""}`);
253
+ const recommendationDetailLines = (proposal.proposal.recommendationTechnologyDetails ?? [])
254
+ .map((tech) => `${tech.entityName ?? tech.entityId ?? "Unknown technology"}${tech.architectureLayer ? ` [${tech.architectureLayer}]` : ""}${tech.source ? ` • ${tech.source}` : ""}${tech.reason ? ` — ${tech.reason}` : ""}`);
255
+ const blueprintLines = (proposal.proposal.recommendationBlueprint ?? [])
256
+ .map((item) => `${item.label ?? item.layer ?? "Unlabelled layer"}${item.coverage ? ` — ${item.coverage}` : ""}${item.recommendationEntityIds.length > 0 ? ` (${item.recommendationEntityIds.length} mapped recommendation${item.recommendationEntityIds.length === 1 ? "" : "s"})` : ""}`);
257
+ const capabilityGapLines = (proposal.proposal.recommendationCapabilityGaps ?? [])
258
+ .map((item) => `${item.capability ?? "Unspecified capability"}${item.layer ? ` [${item.layer}]` : ""}${item.severity ? ` • ${item.severity}` : ""}${item.reason ? ` — ${item.reason}` : ""}`);
259
+ const warningLines = proposal.proposal.recommendationWarnings ?? [];
260
+ const riskLines = proposal.proposal.recommendationRisks ?? [];
236
261
  const policyLines = proposal.policyControls.map((control) => `${control.name}${control.description ? ` — ${control.description}` : ""}`);
237
262
  const gateLines = proposal.proposal.requiresPolicyReview
238
263
  ? `- Activation gate: policy review required\n- Required policy controls: ${proposal.proposal.requiredPolicyControlIds?.length ?? 0}\n`
239
264
  : "";
240
- 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${gateLines}\n${proposal.application.recommendationSummary ? `## Recommendation summary\n\n${proposal.application.recommendationSummary}\n\n` : ""}${proposal.proposal.preScaffoldInstructions ? `## Pre-scaffold instruction\n\n${proposal.proposal.preScaffoldInstructions}\n\n` : ""}## Technology choices\n\n${formatBulletList(techLines)}\n\n## Policy controls\n\n${formatBulletList(policyLines)}\n`;
265
+ const trace = proposal.proposal.recommendationTrace;
266
+ 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- Recommendation status: ${proposal.application.recommendationStatus ?? proposal.proposal.recommendationStatus ?? "n/a"}\n- Recommendation job: ${proposal.application.recommendationJobId ?? proposal.proposal.recommendationJobId ?? "n/a"}\n- Recommendation generated: ${proposal.application.recommendationGeneratedAt ?? proposal.proposal.recommendationGeneratedAt ?? "n/a"}\n- Proposal source: ${proposal.proposal.proposalSource ?? "n/a"}\n${gateLines}\n${proposal.application.recommendationSummary ? `## Recommendation summary\n\n${proposal.application.recommendationSummary}\n\n` : ""}${blueprintLines.length > 0 ? `## Layer blueprint\n\n${formatBulletList(blueprintLines)}\n\n` : ""}${recommendationDetailLines.length > 0 ? `## Recommendation provenance\n\n${formatBulletList(recommendationDetailLines)}\n\n` : ""}${capabilityGapLines.length > 0 ? `## Capability gaps\n\n${formatBulletList(capabilityGapLines)}\n\n` : ""}${warningLines.length > 0 ? `## Warnings\n\n${formatBulletList(warningLines)}\n\n` : ""}${riskLines.length > 0 ? `## Risks\n\n${formatBulletList(riskLines)}\n\n` : ""}${trace ? `## Recommendation trace\n\n- Candidate count: ${trace.candidateCount ?? 0}\n- Reference matches: ${trace.referenceMatchCount ?? 0}\n- Similar applications: ${trace.similarApplicationCount ?? 0}\n\n` : ""}${proposal.proposal.preScaffoldInstructions ? `## Pre-scaffold instruction\n\n${proposal.proposal.preScaffoldInstructions}\n\n` : ""}## Technology choices\n\n${formatBulletList(techLines)}\n\n## Policy controls\n\n${formatBulletList(policyLines)}\n`;
241
267
  }
242
268
  function buildPolicyMarkdown(proposal) {
243
269
  if (proposal.policyControls.length === 0) {
@@ -498,6 +524,9 @@ export async function proposalsStart(args) {
498
524
  const reason = parseOptionValue(args, "--reason") ?? "Application scaffold started from approved proposal workflow";
499
525
  const repoUrl = parseOptionValue(args, "--repo");
500
526
  const creds = requireCredentials();
527
+ const identity = loadIdentity();
528
+ const agentRef = identity.agentKey;
529
+ const lease = agentRef ? selectCheckInLease(creds.companyId, agentRef) : null;
501
530
  const listRaw = await callMcpTool("nexarch_list_proposed_applications", { companyId: creds.companyId }, { companyId: creds.companyId });
502
531
  const list = parseToolText(listRaw);
503
532
  const proposals = list.proposals ?? [];
@@ -567,8 +596,10 @@ export async function proposalsStart(args) {
567
596
  initiatedBy: "nexarch-cli",
568
597
  command: "proposals start",
569
598
  operatorEmail: creds.email,
599
+ agentRef,
570
600
  },
571
601
  reviewedPolicyControlIds: proposal.proposal.requiredPolicyControlIds ?? proposal.proposal.confirmedPolicyControlIds,
602
+ leaseToken: lease?.leaseToken,
572
603
  }, { companyId: creds.companyId });
573
604
  activated = parseToolText(activateRaw);
574
605
  }
@@ -591,6 +622,10 @@ export async function proposalsStart(args) {
591
622
  console.log(`- Runtime : ${scaffold.template.runtime}`);
592
623
  console.log(`- Files : ${scaffold.createdFiles.length} created`);
593
624
  console.log(`- State : ${activated ? `${activated.status}${activated.alreadyActive ? " (already active)" : ""}` : "left in proposed state"}`);
625
+ for (const warning of activated?.warnings ?? []) {
626
+ if (warning.message)
627
+ console.log(`- Warning : ${warning.message}`);
628
+ }
594
629
  console.log("\nNext steps:");
595
630
  scaffold.template.nextSteps.forEach((step, index) => console.log(`${index + 1}. ${step}`));
596
631
  console.log(`${scaffold.template.nextSteps.length + 1}. Use docs/architecture/nexarch-proposal.md and policy-controls.md while building.`);
@@ -1,3 +1,4 @@
1
+ import { getCheckInLeaseState, loadCheckInLease } from "../lib/check-in-lease.js";
1
2
  import { requireCredentials } from "../lib/credentials.js";
2
3
  import { fetchAgentRegistryOrThrow } from "../lib/agent-registry.js";
3
4
  import { callMcpTool } from "../lib/mcp.js";
@@ -6,6 +7,8 @@ export async function status(_args) {
6
7
  const selectedCompanyId = creds.companyId;
7
8
  const expiresAt = new Date(creds.expiresAt);
8
9
  const daysLeft = Math.ceil((expiresAt.getTime() - Date.now()) / (1000 * 60 * 60 * 24));
10
+ const lease = loadCheckInLease();
11
+ const leaseState = lease && lease.companyId === selectedCompanyId ? getCheckInLeaseState(lease) : { status: "missing", expiresInSeconds: null };
9
12
  process.stdout.write("Connecting to mcp.nexarch.ai… ");
10
13
  let governance;
11
14
  let registryInfo = null;
@@ -51,7 +54,19 @@ export async function status(_args) {
51
54
  console.log(` Latest snapshot: none published yet`);
52
55
  }
53
56
  console.log(`\n Token expires: ${expiresAt.toLocaleDateString()} (${daysLeft} days)`);
57
+ if (leaseState.status === "valid" && lease) {
58
+ console.log(` Check-in lease: ${new Date(lease.leaseExpiresAt).toLocaleString()} (${Math.ceil((leaseState.expiresInSeconds ?? 0) / 3600)}h left)`);
59
+ }
60
+ else if (leaseState.status === "expired" && lease) {
61
+ console.log(` Check-in lease: expired at ${new Date(lease.leaseExpiresAt).toLocaleString()}`);
62
+ }
63
+ else {
64
+ console.log(" Check-in lease: none stored");
65
+ }
54
66
  if (daysLeft <= 14) {
55
67
  console.log(`\n ⚠ Your token expires soon. Run \`nexarch login\` to renew.`);
56
68
  }
69
+ if (leaseState.status !== "valid") {
70
+ console.log(" ⚠ Run `nexarch check-in` to refresh your workspace lease.");
71
+ }
57
72
  }
@@ -0,0 +1,55 @@
1
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ function leasePath() {
5
+ return join(homedir(), ".nexarch", "check-in-lease.json");
6
+ }
7
+ export function loadCheckInLease() {
8
+ const path = leasePath();
9
+ if (!existsSync(path))
10
+ return null;
11
+ try {
12
+ const raw = readFileSync(path, "utf8");
13
+ const lease = JSON.parse(raw);
14
+ if (!lease.leaseToken || !lease.companyId || !lease.agentRef || !lease.leaseExpiresAt || !lease.leaseIssuedAt) {
15
+ return null;
16
+ }
17
+ return lease;
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ }
23
+ export function saveCheckInLease(lease) {
24
+ const dir = join(homedir(), ".nexarch");
25
+ mkdirSync(dir, { recursive: true });
26
+ writeFileSync(leasePath(), JSON.stringify(lease, null, 2), {
27
+ encoding: "utf8",
28
+ mode: 0o600,
29
+ });
30
+ }
31
+ export function clearCheckInLease() {
32
+ const path = leasePath();
33
+ if (existsSync(path))
34
+ rmSync(path);
35
+ }
36
+ export function selectCheckInLease(companyId, agentRef) {
37
+ const lease = loadCheckInLease();
38
+ if (!lease)
39
+ return null;
40
+ if (lease.companyId !== companyId)
41
+ return null;
42
+ if (lease.agentRef !== agentRef)
43
+ return null;
44
+ return lease;
45
+ }
46
+ export function getCheckInLeaseState(lease) {
47
+ if (!lease)
48
+ return { status: "missing", expiresInSeconds: null };
49
+ const expiresAtMs = new Date(lease.leaseExpiresAt).getTime();
50
+ const expiresInMs = expiresAtMs - Date.now();
51
+ if (!Number.isFinite(expiresAtMs) || expiresInMs <= 0) {
52
+ return { status: "expired", expiresInSeconds: 0 };
53
+ }
54
+ return { status: "valid", expiresInSeconds: Math.floor(expiresInMs / 1000) };
55
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexarch",
3
- "version": "0.11.4",
3
+ "version": "0.12.0",
4
4
  "description": "Your architecture workspace for AI delivery.",
5
5
  "keywords": [
6
6
  "nexarch",