nexarch 0.12.31 → 0.12.33

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.
@@ -6,8 +6,22 @@ import { saveCredentials } from "../lib/credentials.js";
6
6
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
7
7
  import { homedir } from "os";
8
8
  import { join } from "path";
9
+ import { selectFromList } from "../lib/prompt-select.js";
9
10
  const NEXARCH_URL = "https://nexarch.ai";
10
11
  const LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
12
+ // Mirrors AGENT_CAPABILITY_PROFILES in web/src/lib/mcp-agents.ts — the
13
+ // server is the actual authority on what each profile grants; this is just
14
+ // the same labels/descriptions shown in the enrollment UI, reused here so
15
+ // the CLI prompt and the web form agree about what "Worker" means.
16
+ const PROFILE_CHOICES = [
17
+ { value: "observer", label: "Observer", description: "Read graph, policies, and governance context. Cannot write or claim work." },
18
+ { value: "contributor", label: "Contributor", description: "Observer, plus proposing entities/relationships and new applications." },
19
+ { value: "worker", label: "Worker", description: "Contributor, plus checking in, claiming, and completing commands." },
20
+ { value: "delivery", label: "Delivery", description: "Worker, plus activating proposed applications." },
21
+ ];
22
+ async function promptCapabilityProfile() {
23
+ return selectFromList("Select a capability profile for this credential:", PROFILE_CHOICES, "worker");
24
+ }
11
25
  function printLoginBanner() {
12
26
  const logo = String.raw `
13
27
  ###### ######
@@ -136,10 +150,14 @@ export async function login(args) {
136
150
  const state = generateState();
137
151
  const port = await findFreePort();
138
152
  const requestedCompany = getArgValue(args, "--company");
139
- const qp = new URLSearchParams({ port: String(port), state });
153
+ const requestedProfile = getArgValue(args, "--profile");
154
+ const isCapabilityProfile = (value) => PROFILE_CHOICES.some((c) => c.value === value);
155
+ const profile = isCapabilityProfile(requestedProfile) ? requestedProfile : await promptCapabilityProfile();
156
+ const qp = new URLSearchParams({ port: String(port), state, profile });
140
157
  if (requestedCompany)
141
158
  qp.set("company", requestedCompany);
142
159
  const authUrl = `${NEXARCH_URL}/auth/cli?${qp.toString()}`;
160
+ console.log(`\nCapability profile: ${profile}`);
143
161
  console.log("Opening Nexarch in your browser…");
144
162
  console.log(`\n ${authUrl}\n`);
145
163
  console.log("If the browser did not open, copy the URL above and paste it in manually.\n");
@@ -0,0 +1,72 @@
1
+ import readline from "readline";
2
+ /**
3
+ * Arrow-key single-select prompt. No dependency — this package has none for
4
+ * a reason (see package.json), and Node's own readline keypress events cover
5
+ * the whole interaction: up/down to move, enter to confirm, ctrl+c to abort.
6
+ *
7
+ * Falls back to `defaultValue` without prompting when stdin/stdout isn't a
8
+ * TTY (piped input, --non-interactive runs, CI) — the same guard setup.ts
9
+ * already uses for its yes/no prompt.
10
+ */
11
+ export async function selectFromList(title, choices, defaultValue) {
12
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
13
+ return defaultValue;
14
+ }
15
+ const defaultIndex = choices.findIndex((c) => c.value === defaultValue);
16
+ let index = defaultIndex >= 0 ? defaultIndex : 0;
17
+ let linesDrawn = 0;
18
+ const render = () => {
19
+ if (linesDrawn > 0) {
20
+ readline.moveCursor(process.stdout, 0, -linesDrawn);
21
+ readline.cursorTo(process.stdout, 0);
22
+ readline.clearScreenDown(process.stdout);
23
+ }
24
+ const lines = [
25
+ `${title} (↑/↓ to move, enter to select)`,
26
+ ...choices.flatMap((choice, i) => {
27
+ const selected = i === index;
28
+ const pointer = selected ? "❯ " : " ";
29
+ const label = selected ? `\x1b[1m${choice.label}\x1b[0m` : choice.label;
30
+ return [`${pointer}${label}`, ` ${choice.description}`];
31
+ }),
32
+ ];
33
+ process.stdout.write(lines.join("\n") + "\n");
34
+ linesDrawn = lines.length;
35
+ };
36
+ return new Promise((resolve) => {
37
+ readline.emitKeypressEvents(process.stdin);
38
+ const wasRaw = process.stdin.isRaw;
39
+ process.stdin.setRawMode(true);
40
+ process.stdin.resume();
41
+ const cleanup = () => {
42
+ process.stdin.removeListener("keypress", onKeypress);
43
+ process.stdin.setRawMode(Boolean(wasRaw));
44
+ process.stdin.pause();
45
+ };
46
+ const onKeypress = (_str, key) => {
47
+ if (!key)
48
+ return;
49
+ if (key.ctrl && key.name === "c") {
50
+ cleanup();
51
+ console.log();
52
+ process.exit(130);
53
+ }
54
+ if (key.name === "up") {
55
+ index = (index - 1 + choices.length) % choices.length;
56
+ render();
57
+ return;
58
+ }
59
+ if (key.name === "down") {
60
+ index = (index + 1) % choices.length;
61
+ render();
62
+ return;
63
+ }
64
+ if (key.name === "return") {
65
+ cleanup();
66
+ resolve(choices[index].value);
67
+ }
68
+ };
69
+ render();
70
+ process.stdin.on("keypress", onKeypress);
71
+ });
72
+ }
@@ -120,6 +120,21 @@ skill is the check-in → claim → complete loop for working that queue.
120
120
  \`nexarch_fail_command\` with a reason if it couldn't be done. Never leave a
121
121
  claimed command unresolved at the end of a session.
122
122
 
123
+ ## If check-in shows a command you can't claim
124
+
125
+ Claiming/completing/failing a command needs the \`mcp:work:commands\` scope —
126
+ a separate, higher capability profile ("worker" or above) than what does the
127
+ graph writing (\`mcp:write:discovery\`, granted from "contributor" up). A
128
+ credential can see pending commands and still lack the scope to claim one;
129
+ check-in's response says so explicitly when that's the case
130
+ (\`claimReason: "credential_missing_work_scope"\`). If you hit this: still do
131
+ the work if it's clearly what's needed, but say in your summary that you
132
+ couldn't formally claim/complete the command, and that a human needs to
133
+ either handle it in the workspace or reissue your credential with the
134
+ "worker" profile. Don't call \`nexarch_claim_command_by_id\` speculatively —
135
+ if your tools/list doesn't include it, your credential doesn't have the
136
+ scope, and the call will fail.
137
+
123
138
  ## Ground rules
124
139
 
125
140
  - \`nexarch_claim_command\` is a legacy alias for \`nexarch_check_in\` — use
@@ -177,6 +192,83 @@ evidence — never invent a compliance status that isn't stored.
177
192
  what you found and let the human decide, same as the reuse-before-build
178
193
  principle elsewhere in Nexarch.
179
194
  `;
195
+ const DECISION_RECORDS_SKILL_BODY = `---
196
+ name: nexarch-decision-records
197
+ description: Record an architectural decision in Nexarch, or check what's already been decided about something. Use when a decision is being made (a technology, pattern, or approach chosen over alternatives), when ADR/RFC documents are found in a repository, when asked what's already been decided about a topic, or when a new decision replaces an older one. Requires the Nexarch MCP tools (nexarch_*).
198
+ ---
199
+
200
+ # Nexarch Decision Records
201
+
202
+ Decisions are first-class in Nexarch: \`decision_record\` entities linked to
203
+ whatever they concern via \`decides\`, reviewed in the workspace's Decisions
204
+ page, and optionally promoted into workspace-wide policy by a human. This
205
+ skill is for recording one as it happens — mining a whole repository's ADR
206
+ history systematically is a separate, human-triggered Decision Review
207
+ command from the application's page in the workspace.
208
+
209
+ ## Before recording
210
+
211
+ \`nexarch_resolve_reference\` / \`nexarch_list_entities\` for an existing
212
+ \`decision_record\` covering the same choice — match by name/summary first.
213
+ Reuse and refresh it rather than creating a duplicate for the same decision.
214
+
215
+ ## Recording a decision
216
+
217
+ \`entityTypeCode: "decision_record"\`. Two attributes are strictly required —
218
+ MCP rejects the write with \`INVALID_DECISION_RECORD_PAYLOAD\` if either is
219
+ missing or empty, with no fallback to \`description\` (ADR-0101):
220
+
221
+ - \`attributes.decision.summary\` — one sentence naming what was chosen.
222
+ - \`attributes.decision.detail\` — the fuller rationale, cited to its evidence.
223
+
224
+ Never invent a decision from a technology's mere presence — it must trace to
225
+ an actual stated reason someone chose it over an alternative.
226
+
227
+ ### Optional fields — add them when the evidence actually supports them
228
+
229
+ All additive; a decision recorded with only summary/detail is still
230
+ completely valid. Leave a field unset rather than guessing it:
231
+
232
+ - \`attributes.source.repositoryUrl\` / \`.commit\` / \`.path\` / \`.lines\` — where
233
+ the decision was found, so the workspace can show exact evidence and
234
+ detect when it drifts from what's now in the repo.
235
+ - \`attributes.decision.status\` — one of \`unknown\` / \`proposed\` / \`accepted\`
236
+ / \`deprecated\` / \`superseded\` / \`rejected\`, only when the source states it
237
+ explicitly.
238
+ - \`attributes.decision.rationale\` — the "why", as its own field, when it's
239
+ distinguishable from the alternatives/consequences discussion.
240
+ - \`attributes.decision.alternatives\` — array of \`{"option": "...",
241
+ "whyRejected": "..."}\` for alternatives the source explicitly names and
242
+ explains rejecting. Don't invent alternatives the source never mentions.
243
+ - \`attributes.decision.consequences\` — array of plain-string trade-offs the
244
+ source explicitly states.
245
+ - \`attributes.decision.supersededByRef\` — the \`entityRef\` of the
246
+ \`decision_record\` that replaces this one, only when the source explicitly
247
+ says so. Setting this on an upsert automatically flags the old decision as
248
+ superseded in the workspace — you don't need to touch the old record
249
+ yourself.
250
+
251
+ ### Linking
252
+
253
+ \`decision_record -> decides -> <application | technology_component |
254
+ platform | api | other ontology-valid target>\` — the most specific existing
255
+ entity the decision actually concerns. Only link to entities that already
256
+ exist; this skill records decisions, it doesn't discover architecture. If
257
+ nothing ontology-valid exists yet to link to, still record the decision and
258
+ say so rather than skipping it or inventing a target.
259
+
260
+ ## Ground rules
261
+
262
+ - A decision being implemented is not the same as a decision being followed
263
+ correctly — recording one here says nothing about whether the code
264
+ actually conforms. That's \`nexarch_submit_decision_conformance\` (see the
265
+ \`nexarch-governance-review\` skill), a separate, evidence-based check.
266
+ - Conflicts, duplicates, and workspace-wide promotion of a decision are
267
+ reviewed by a human in the workspace UI, not something to resolve or
268
+ decide yourself — your job is accurate recording and evidenced linking.
269
+ - End with a one-line summary: what was recorded or reused, and what (if
270
+ anything) couldn't be linked.
271
+ `;
180
272
  export const SKILLS = [
181
273
  {
182
274
  templateCode: "nexarch_claude_code_skill_v1",
@@ -198,6 +290,11 @@ export const SKILLS = [
198
290
  dirName: "nexarch-governance-review",
199
291
  fallbackBody: GOVERNANCE_REVIEW_SKILL_BODY,
200
292
  },
293
+ {
294
+ templateCode: "nexarch_decision_records_skill_v1",
295
+ dirName: "nexarch-decision-records",
296
+ fallbackBody: DECISION_RECORDS_SKILL_BODY,
297
+ },
201
298
  ];
202
299
  const RUNTIME_SKILLS_ROOT = {
203
300
  "claude-code": [".claude", "skills"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexarch",
3
- "version": "0.12.31",
3
+ "version": "0.12.33",
4
4
  "description": "Your architecture workspace for AI delivery.",
5
5
  "keywords": [
6
6
  "nexarch",