@aiwg/cli 2026.10.0 → 2026.10.3

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 (168) hide show
  1. package/agentic/code/providers/capability-matrix.yaml +270 -0
  2. package/agentic/code/providers/model-capabilities.v1.json +3091 -24
  3. package/bin/aiwg.mjs +86 -21
  4. package/dist/src/a2a/codecs.js +9 -0
  5. package/dist/src/artifacts/corpus-tools/vision-extract.js +105 -15
  6. package/dist/src/artifacts/repair.js +9 -4
  7. package/dist/src/channel/manager.mjs +59 -10
  8. package/dist/src/cli/child-process-result.js +40 -0
  9. package/dist/src/cli/env.js +38 -16
  10. package/dist/src/cli/flag-parsing.js +236 -0
  11. package/dist/src/cli/handlers/decision.js +297 -2
  12. package/dist/src/cli/handlers/diagnose.js +9 -2
  13. package/dist/src/cli/handlers/feedback.js +14 -5
  14. package/dist/src/cli/handlers/init.js +245 -30
  15. package/dist/src/cli/handlers/mc.js +23 -0
  16. package/dist/src/cli/handlers/models.js +69 -6
  17. package/dist/src/cli/handlers/ralph.js +3 -0
  18. package/dist/src/cli/handlers/refresh.js +16 -0
  19. package/dist/src/cli/handlers/regenerate.js +14 -0
  20. package/dist/src/cli/handlers/runtime-info.js +24 -9
  21. package/dist/src/cli/handlers/script-runner.js +16 -6
  22. package/dist/src/cli/handlers/session.js +396 -62
  23. package/dist/src/cli/handlers/sessions.js +146 -40
  24. package/dist/src/cli/handlers/setup-manifest.js +439 -39
  25. package/dist/src/cli/handlers/setup.js +139 -39
  26. package/dist/src/cli/handlers/subcommands.js +3 -1
  27. package/dist/src/cli/handlers/team.js +18 -8
  28. package/dist/src/cli/handlers/use.js +13 -0
  29. package/dist/src/cli/handlers/utilities.js +106 -23
  30. package/dist/src/cli/prompt-utils.js +72 -0
  31. package/dist/src/cli/provider-resolution.js +11 -0
  32. package/dist/src/config/aiwg-config.js +268 -4
  33. package/dist/src/config/cli.js +146 -19
  34. package/dist/src/config/dispatch-thresholds-runtime.mjs +38 -0
  35. package/dist/src/config/project-artifacts-health.mjs +14 -3
  36. package/dist/src/config/user-config.js +36 -10
  37. package/dist/src/decision/adapters/jev.js +80 -28
  38. package/dist/src/decision/ask.js +352 -0
  39. package/dist/src/decision/driver.js +4 -0
  40. package/dist/src/decision/entry.js +38 -6
  41. package/dist/src/decision/heldout/collector.js +10 -1
  42. package/dist/src/decision/index.js +3 -0
  43. package/dist/src/decision/interop.js +60 -8
  44. package/dist/src/decision/jev-credentials.js +121 -0
  45. package/dist/src/decision/ledger.js +839 -0
  46. package/dist/src/decision/validate.js +6 -3
  47. package/dist/src/effects/dispatch-verifier.js +252 -0
  48. package/dist/src/effects/index.js +1 -0
  49. package/dist/src/features/installer.js +2 -2
  50. package/dist/src/flow/graph-metadata.js +5 -0
  51. package/dist/src/gates/providers/loader.js +3 -1
  52. package/dist/src/installation/manager-command.mjs +1 -1
  53. package/dist/src/installation/manager.mjs +17 -2
  54. package/dist/src/lint/runner.js +2 -2
  55. package/dist/src/mcp/adapters/codex-runtime.js +360 -56
  56. package/dist/src/mcp/cli.mjs +587 -77
  57. package/dist/src/mcp/config-file.mjs +279 -0
  58. package/dist/src/mcp/config-layers.mjs +348 -0
  59. package/dist/src/mcp/credentials.mjs +484 -0
  60. package/dist/src/mcp/grok-build-config.mjs +16 -26
  61. package/dist/src/mcp/helpers.mjs +106 -2
  62. package/dist/src/mcp/omp-config.mjs +50 -29
  63. package/dist/src/mcp/profiles.js +97 -23
  64. package/dist/src/mcp/profiles.mjs +99 -27
  65. package/dist/src/mcp/registry.js +264 -57
  66. package/dist/src/mcp/registry.mjs +198 -45
  67. package/dist/src/mcp/server.mjs +403 -20
  68. package/dist/src/mcp/toml-string.mjs +9 -0
  69. package/dist/src/mcp/toml-strip-mcp.mjs +135 -0
  70. package/dist/src/mcp/tool-filters.mjs +357 -0
  71. package/dist/src/mcp/tools/agentic-sandbox.mjs +3 -1
  72. package/dist/src/mcp/tools/decision.mjs +58 -0
  73. package/dist/src/mcp/tools/orchestration.mjs +38 -0
  74. package/dist/src/mcp/tools/subsystems.mjs +39 -8
  75. package/dist/src/mcp/tools/worker-invoke.mjs +1539 -0
  76. package/dist/src/mcp/tools/worker-shell-control.mjs +258 -0
  77. package/dist/src/mcp/tools/worker-stdin-runtime.mjs +79 -0
  78. package/dist/src/memory/project-registry.js +2 -2
  79. package/dist/src/mission-protocol/codecs.js +639 -1
  80. package/dist/src/mission-protocol/types.js +110 -0
  81. package/dist/src/mission-protocol/worker-status.js +2 -0
  82. package/dist/src/models/config-loader.js +70 -13
  83. package/dist/src/models/grok-build-models.js +93 -41
  84. package/dist/src/models/model-capabilities.v1.json +3091 -24
  85. package/dist/src/models/model-discovery.js +862 -63
  86. package/dist/src/models/provider-policy.js +625 -11
  87. package/dist/src/models/qualification-digest.mjs +17 -0
  88. package/dist/src/models/resolver.js +259 -0
  89. package/dist/src/models/worker-direct-readiness.js +529 -0
  90. package/dist/src/models/worker-dispatch-offer.js +169 -0
  91. package/dist/src/models/worker-health-readiness.js +131 -0
  92. package/dist/src/models/worker-installation.mjs +879 -0
  93. package/dist/src/models/wrapper-route.js +18 -0
  94. package/dist/src/ops/registry.js +9 -14
  95. package/dist/src/plugins/standalone-packager.js +4 -3
  96. package/dist/src/process/bounded-child-process.mjs +387 -0
  97. package/dist/src/process/cli-command.mjs +75 -0
  98. package/dist/src/providers/capability-matrix.js +8 -0
  99. package/dist/src/providers/capability-matrix.yaml +270 -0
  100. package/dist/src/providers/grok-build-headless.js +43 -21
  101. package/dist/src/providers/provider-definitions.js +9 -1
  102. package/dist/src/providers/provider-definitions.mjs +9 -2
  103. package/dist/src/serve/executor-registry.js +16 -24
  104. package/dist/src/serve/fleet-mission-conductor.js +9 -0
  105. package/dist/src/serve/mission-conductor.js +9 -0
  106. package/dist/src/serve/shared-host-scheduler.js +2 -0
  107. package/dist/src/serve/worker-invocation.js +538 -0
  108. package/dist/src/serve/worker-stdin-frame.mjs +107 -0
  109. package/dist/src/sessions/adapter-registry.js +45 -0
  110. package/dist/src/sessions/adapters/generic.js +37 -3
  111. package/dist/src/sessions/adapters/grokbot-locator.js +111 -0
  112. package/dist/src/sessions/adapters/grokbot.js +423 -24
  113. package/dist/src/sessions/adapters/muse-workspace-scan.js +108 -0
  114. package/dist/src/sessions/adapters/muse.js +68 -29
  115. package/dist/src/sessions/adapters/pi.js +1 -0
  116. package/dist/src/sessions/batch-contracts.js +13 -1
  117. package/dist/src/sessions/batch-import.js +23 -42
  118. package/dist/src/sessions/candidates.js +47 -11
  119. package/dist/src/sessions/contracts.js +35 -0
  120. package/dist/src/sessions/coverage-preflight.js +134 -0
  121. package/dist/src/sessions/discovery.js +3 -0
  122. package/dist/src/sessions/grok-build-cli.js +67 -5
  123. package/dist/src/sessions/importer.js +24 -8
  124. package/dist/src/sessions/index.js +2 -0
  125. package/dist/src/sessions/repository.js +54 -13
  126. package/dist/src/sessions/workspace-discovery.js +99 -37
  127. package/dist/src/skills/adapters/clawhub.js +10 -8
  128. package/dist/src/skills/adapters/openclaw.js +10 -7
  129. package/dist/src/smiths/context-pipeline/parallelism-section.js +192 -3
  130. package/dist/src/smiths/context-pipeline/workspace-context.js +116 -9
  131. package/dist/src/smiths/hook-bridge/codex-translator.js +6 -5
  132. package/dist/src/smiths/mcpsmith/example.js +1 -1
  133. package/dist/src/smiths/toolsmith/runtime-discovery.mjs +10 -14
  134. package/dist/src/storage/backends/logseq.js +68 -7
  135. package/dist/src/storage/fortemi-qualification-receipt.js +2 -2
  136. package/dist/src/testing/generators/test-code-generator.js +57 -30
  137. package/dist/src/uhp/client.js +11 -1
  138. package/dist/src/uhp/mission.js +5 -0
  139. package/dist/src/update/checker.mjs +5 -5
  140. package/dist/src/update/notifier.mjs +54 -2
  141. package/dist/src/writing/fidelity.js +18 -1
  142. package/package.json +18 -1
  143. package/schemas/mission-v1.schema.json +2429 -0
  144. package/schemas/models/model-catalog.v1.schema.json +301 -0
  145. package/schemas/models/model-config-compatibility.v1.schema.json +281 -0
  146. package/schemas/models/model-policy.v1.schema.json +14 -0
  147. package/schemas/models/provider-model-capabilities.v1.schema.json +1066 -0
  148. package/tools/_resolve-impl.mjs +39 -0
  149. package/tools/agents/deploy-agents.mjs +15 -2
  150. package/tools/agents/providers/base.mjs +63 -43
  151. package/tools/agents/providers/grokbot.mjs +11 -12
  152. package/tools/agents/providers/hermes.mjs +6 -3
  153. package/tools/agents/providers/omp.mjs +5 -2
  154. package/tools/agents/providers/openclaw-translator.mjs +19 -5
  155. package/tools/providers/deepseek-harness-transport.mjs +358 -0
  156. package/tools/providers/omp-transport.mjs +192 -0
  157. package/tools/ralph-external/lib/claude-adapter.mjs +389 -0
  158. package/tools/ralph-external/lib/codex-adapter.mjs +432 -0
  159. package/tools/ralph-external/lib/deepseek-harness-adapter.mjs +397 -0
  160. package/tools/ralph-external/lib/factory-adapter.mjs +503 -0
  161. package/tools/ralph-external/lib/muse-adapter.mjs +364 -0
  162. package/tools/ralph-external/lib/omp-adapter.mjs +315 -0
  163. package/tools/ralph-external/lib/opencode-adapter.mjs +575 -0
  164. package/tools/ralph-external/lib/openhuman-adapter.mjs +75 -0
  165. package/tools/ralph-external/lib/pi-adapter.mjs +333 -0
  166. package/tools/ralph-external/lib/provider-adapter.mjs +690 -0
  167. package/tools/ralph-external/lib/worker-health.mjs +439 -0
  168. package/vscode-extension/schemas/aiwg.config.v1.json +1953 -0
@@ -25,9 +25,24 @@
25
25
  *
26
26
  * Handlers MUST call this helper instead of re-deriving the check locally.
27
27
  * Divergent isTTY checks scattered across handlers have caused bugs in the
28
- * past (#918 audit findings).
28
+ * past (#918 audit findings, and #3062 review item 12: `init` only checked
29
+ * `CI === 'true'`, `setup project` ignored CI/TERM entirely, and `session`
30
+ * ignored its own `--non-interactive` flag — each reinventing a narrower
31
+ * version of this same check instead of using it).
32
+ *
33
+ * @param explicitNonInteractive When `true`, short-circuits to `false`
34
+ * without inspecting the environment at all. Pass the caller's own
35
+ * explicit non-interactive flag here (e.g. `--non-interactive`, or
36
+ * `--yes` where it means "don't prompt") so the single interactive
37
+ * decision takes the command's explicit flags as input, rather than
38
+ * every call site re-deriving its own mix of flag-vs-environment
39
+ * precedence. Omit it (or pass `false`) to fall through to the
40
+ * environment-only checks below, unchanged from before this parameter
41
+ * existed.
29
42
  */
30
- export function isInteractive() {
43
+ export function isInteractive(explicitNonInteractive = false) {
44
+ if (explicitNonInteractive)
45
+ return false;
31
46
  if (!process.stdin.isTTY)
32
47
  return false;
33
48
  if (!process.stdout.isTTY)
@@ -40,25 +55,32 @@ export function isInteractive() {
40
55
  return false;
41
56
  return true;
42
57
  }
58
+ /**
59
+ * Variables that mark a continuous-integration environment. Exported so tests
60
+ * that need the interactive path can clear exactly what `isCI()` reads.
61
+ */
62
+ export const CI_ENV_VARS = [
63
+ 'CI',
64
+ 'CONTINUOUS_INTEGRATION',
65
+ 'BUILD_NUMBER',
66
+ 'RUN_ID',
67
+ 'GITHUB_ACTIONS',
68
+ 'GITLAB_CI',
69
+ 'CIRCLECI',
70
+ 'TRAVIS',
71
+ 'BUILDKITE',
72
+ 'JENKINS_URL',
73
+ 'TF_BUILD', // Azure DevOps
74
+ 'TEAMCITY_VERSION',
75
+ ];
76
+ /** Other variables `isInteractive()` reads. */
77
+ export const INTERACTIVE_ENV_VARS = ['TERM', 'DEBIAN_FRONTEND'];
43
78
  /**
44
79
  * Return true iff we're running in a continuous-integration environment.
45
80
  * Checks the common CI env vars set by major CI systems.
46
81
  */
47
82
  export function isCI() {
48
- const ciVars = [
49
- 'CI',
50
- 'CONTINUOUS_INTEGRATION',
51
- 'BUILD_NUMBER',
52
- 'RUN_ID',
53
- 'GITHUB_ACTIONS',
54
- 'GITLAB_CI',
55
- 'CIRCLECI',
56
- 'TRAVIS',
57
- 'BUILDKITE',
58
- 'JENKINS_URL',
59
- 'TF_BUILD', // Azure DevOps
60
- 'TEAMCITY_VERSION',
61
- ];
83
+ const ciVars = CI_ENV_VARS;
62
84
  for (const v of ciVars) {
63
85
  const val = process.env[v];
64
86
  if (val && val !== '0' && val.toLowerCase() !== 'false')
@@ -0,0 +1,236 @@
1
+ /**
2
+ * Shared strict CLI flag/value parsing (#3062 review item 11, and the
3
+ * second-round review's finding 4).
4
+ *
5
+ * Several handlers (`init`, `setup`, `session`, `setup-run`) each grew
6
+ * their own small `flagValue(args, name)` helper, and every one of them
7
+ * treated a flag that is present but missing its value the same as a flag
8
+ * that was never given at all — `--prompt` as the very last token, or
9
+ * immediately followed by another flag, silently resolved to `undefined`
10
+ * instead of erroring. At a terminal that let the handler fall through
11
+ * into an interactive prompt instead of reporting the mistake; without one
12
+ * it could proceed on a silently-wrong default.
13
+ *
14
+ * The first-round shared parser fixed that but introduced its own gaps,
15
+ * found by the second review:
16
+ *
17
+ * - It validated only the *first* occurrence of a repeated single-value
18
+ * flag, so `--provider claude --provider` (a malformed second
19
+ * occurrence) silently passed.
20
+ * - It always resolved a repeated flag to its first occurrence, which
21
+ * silently changed `aiwg session`'s pre-existing last-occurrence-wins
22
+ * precedence for `--provider`/`--profile` to first-wins — a real
23
+ * behavior regression, not just a new validation gap.
24
+ * - `--flag=value` was never recognized at all.
25
+ * - Scanning never stopped at a bare `--` terminator.
26
+ * - An empty-string value (`--prompt ''`) was accepted as a valid value
27
+ * for flags where that is never meaningful.
28
+ *
29
+ * {@link scanFlagOccurrences} is the single-pass tokenizer every other
30
+ * helper here is built on: it finds *every* occurrence of the named
31
+ * flag(s), in one scan, validating each one independently, and stops
32
+ * recognizing flags entirely once it sees a bare `--` token (everything
33
+ * after that point is positional, per POSIX convention). `--flag value`
34
+ * and `--flag=value` are both accepted; in the space-separated form a
35
+ * value that itself looks like another flag (starts with `--`) is treated
36
+ * as a missing value — pass it via the `=` form instead
37
+ * (`--flag=--literal`) when a value must start with `--`. A value starting
38
+ * with a single `-` is always accepted in either form.
39
+ *
40
+ * Callers choose `precedence: 'first' | 'last'` to match whatever
41
+ * precedence the command already had before this module existed — see
42
+ * each call site's own comment for which one applies and why. Callers
43
+ * MUST catch {@link MissingFlagValueError} (or let it propagate to a
44
+ * handler-level catch) and report it before any preflight check, spawn,
45
+ * or write — never after.
46
+ */
47
+ /** Thrown when a recognized flag is present but has no usable value following it. */
48
+ export class MissingFlagValueError extends Error {
49
+ /** The exact flag token that was missing its value (e.g. `--prompt`). */
50
+ flagName;
51
+ constructor(flagName) {
52
+ super(`${flagName} requires a value`);
53
+ this.name = 'MissingFlagValueError';
54
+ this.flagName = flagName;
55
+ }
56
+ }
57
+ function splitEquals(token) {
58
+ const eq = token.indexOf('=');
59
+ if (eq === -1)
60
+ return null;
61
+ return [token.slice(0, eq), token.slice(eq + 1)];
62
+ }
63
+ /**
64
+ * Single-pass scan for every occurrence of any of `names` in `args`. See
65
+ * the module doc comment for the full set of semantics this implements.
66
+ *
67
+ * Throws {@link MissingFlagValueError} on the *first* occurrence (in
68
+ * left-to-right order) that has no usable value or an empty one (when
69
+ * `options.rejectEmpty`) — every occurrence is checked, not just the
70
+ * first one found overall, so a malformed *later* occurrence is still an
71
+ * error even when an earlier one was valid.
72
+ */
73
+ export function scanFlagOccurrences(args, names, options = {}) {
74
+ const occurrences = [];
75
+ for (let i = 0; i < args.length; i += 1) {
76
+ const token = args[i];
77
+ if (token === undefined)
78
+ continue;
79
+ if (token === '--')
80
+ break; // bare terminator — stop recognizing flags entirely
81
+ if (token.startsWith('--')) {
82
+ const eqSplit = splitEquals(token);
83
+ if (eqSplit && names.includes(eqSplit[0])) {
84
+ const [flag, value] = eqSplit;
85
+ if (options.rejectEmpty && value === '')
86
+ throw new MissingFlagValueError(flag);
87
+ occurrences.push({ index: i, flag, value });
88
+ continue;
89
+ }
90
+ }
91
+ if (names.includes(token)) {
92
+ const next = args[i + 1];
93
+ if (next === undefined || next.startsWith('--')) {
94
+ throw new MissingFlagValueError(token);
95
+ }
96
+ if (options.rejectEmpty && next === '') {
97
+ throw new MissingFlagValueError(token);
98
+ }
99
+ occurrences.push({ index: i, flag: token, value: next });
100
+ i += 1; // skip the consumed value token
101
+ }
102
+ }
103
+ return occurrences;
104
+ }
105
+ /**
106
+ * Look up the value of a (possibly repeated) single-value flag.
107
+ *
108
+ * - None of `names` appear at all → `undefined` (the flag was never
109
+ * given; not an error).
110
+ * - `names` appear once or more, every occurrence validated → the first
111
+ * or last occurrence's value, per `options.precedence` (default
112
+ * `'first'`). Pass `'last'` only for a command whose pre-existing
113
+ * behavior (before this module existed) already let a later occurrence
114
+ * override an earlier one — see the call site's comment.
115
+ */
116
+ export function requireFlagValue(args, options, ...names) {
117
+ const occurrences = scanFlagOccurrences(args, names, options);
118
+ if (occurrences.length === 0)
119
+ return undefined;
120
+ return options.precedence === 'last'
121
+ ? occurrences[occurrences.length - 1].value
122
+ : occurrences[0].value;
123
+ }
124
+ /**
125
+ * Same validation contract as {@link requireFlagValue}, but collects the
126
+ * value of *every* occurrence in left-to-right order — for repeatable
127
+ * flags such as `--param`/`--script`. Returns `[]` when the flag never
128
+ * appears.
129
+ */
130
+ export function collectFlagValues(args, options, ...names) {
131
+ return scanFlagOccurrences(args, names, options).map((occurrence) => occurrence.value);
132
+ }
133
+ /**
134
+ * Parse a strict `true`/`false` flag value. `undefined` in means the flag
135
+ * was not given (not an error, and not a default — callers decide the
136
+ * default). Anything other than exactly `true`/`false` throws, naming
137
+ * `flagName` in the message.
138
+ */
139
+ export function parseBooleanFlagValue(value, flagName) {
140
+ if (value === undefined)
141
+ return undefined;
142
+ if (value === 'true')
143
+ return true;
144
+ if (value === 'false')
145
+ return false;
146
+ throw new Error(`${flagName} must be true or false (got '${value}')`);
147
+ }
148
+ /**
149
+ * Parse a comma-separated list flag value, trimming whitespace and
150
+ * dropping empty entries. Returns `undefined` when `raw` is `undefined`
151
+ * (flag not given); returns `[]` only if every entry was empty.
152
+ */
153
+ export function parseListFlagValue(raw) {
154
+ if (raw === undefined)
155
+ return undefined;
156
+ return raw.split(',').map((entry) => entry.trim()).filter(Boolean);
157
+ }
158
+ /**
159
+ * Split one repeatable `<name>=<value>` entry (as used by `--script`,
160
+ * `--param`) into its parts. Throws, naming `flagName`, when `entry` has
161
+ * no `=`, an empty name, or an empty value.
162
+ */
163
+ export function parseKeyValueEntry(entry, flagName) {
164
+ const eq = entry.indexOf('=');
165
+ if (eq <= 0 || eq === entry.length - 1) {
166
+ throw new Error(`${flagName} requires the form <name>=<value> (got '${entry}')`);
167
+ }
168
+ return [entry.slice(0, eq).trim(), entry.slice(eq + 1).trim()];
169
+ }
170
+ /**
171
+ * The index of the first bare `--` terminator in `args`, or `args.length`
172
+ * when there is none — a loop bounded by this never looks at anything
173
+ * after the terminator, which per POSIX convention is never a flag or a
174
+ * command word again (#3062 go/no-go review, blocker 4).
175
+ */
176
+ export function argsTerminatorIndex(args) {
177
+ const index = args.indexOf('--');
178
+ return index === -1 ? args.length : index;
179
+ }
180
+ /**
181
+ * The set of array indexes in `args` consumed as the *value* of a
182
+ * `--flag value` (two-token) occurrence of any name in `valueFlagNames`
183
+ * (#3062 go/no-go review, blocker 4). A `--flag=value` (one-token) form
184
+ * has no separate value index to report — the value is part of the same
185
+ * token, so it can never coincidentally equal a bare positional word or a
186
+ * different flag's own name the way a space-separated value can.
187
+ *
188
+ * This does not validate occurrences (a missing/malformed value is the
189
+ * paired value-flag parser's own job — e.g. {@link requireFlagValue} on
190
+ * the same `args` and `valueFlagNames`, called separately); it exists
191
+ * purely so a positional/switch scan knows which indexes are already
192
+ * spoken for and must not be reinterpreted as something else.
193
+ */
194
+ export function valueTokenIndexes(args, valueFlagNames) {
195
+ const consumed = new Set();
196
+ const limit = args.length;
197
+ for (let i = 0; i < limit; i += 1) {
198
+ const token = args[i];
199
+ if (token === undefined)
200
+ continue;
201
+ if (token === '--')
202
+ break;
203
+ if (token.startsWith('--') && token.includes('='))
204
+ continue; // value is part of this same token
205
+ if (valueFlagNames.includes(token) && i + 1 < limit) {
206
+ consumed.add(i + 1);
207
+ i += 1; // the value token itself is spoken for; do not also treat it as a flag name
208
+ }
209
+ }
210
+ return consumed;
211
+ }
212
+ /**
213
+ * True iff `name` appears as a bare positional token or an exact-match
214
+ * boolean switch anywhere in `args` *before* the `--` terminator, and is
215
+ * not itself the already-consumed value of one of `valueFlagNames` (#3062
216
+ * go/no-go review, blocker 4).
217
+ *
218
+ * Use this instead of `args.includes(name)` (or an equivalent hand-rolled
219
+ * loop) for any boolean switch or positional command word that a value-
220
+ * taking flag's own value could coincidentally equal — e.g. a free-text
221
+ * `--prompt` argument that happens to contain the literal word a command
222
+ * also uses as a switch, or a value that collides with another flag's own
223
+ * name.
224
+ */
225
+ export function hasPositionalOrSwitch(args, name, valueFlagNames) {
226
+ const consumed = valueTokenIndexes(args, valueFlagNames);
227
+ const limit = argsTerminatorIndex(args);
228
+ for (let i = 0; i < limit; i += 1) {
229
+ if (consumed.has(i))
230
+ continue;
231
+ if (args[i] === name)
232
+ return true;
233
+ }
234
+ return false;
235
+ }
236
+ //# sourceMappingURL=flag-parsing.js.map
@@ -1,10 +1,11 @@
1
- import { decisionCapabilities, evaluateRequestPath, listPatterns, livePlan, materializeSyntheticClassificationSetup, runOfflinePattern, showPattern, syntheticClassificationSetup, validateDecisionInput, } from '../../decision/driver.js';
1
+ import { ATTRIBUTION_FIELD_PATTERN, askDecision, DecisionAskUsageError, decisionCapabilities, DecisionLedgerSinceError, decisionLedgerHistory, decisionLedgerStats, evaluateRequestPath, listPatterns, livePlan, MAX_ATTRIBUTION_FIELD_LENGTH, materializeSyntheticClassificationSetup, parseDecisionLedgerSince, readDecisionContextFile, recordDecisionAsk, runOfflinePattern, setupJev, showPattern, syntheticClassificationSetup, validateDecisionInput, } from '../../decision/driver.js';
2
2
  function jsonResult(value, exitCode = 0) {
3
3
  return { exitCode, message: JSON.stringify(value, null, 2), rawOutput: true };
4
4
  }
5
5
  function usage() {
6
6
  return [
7
- 'Usage: aiwg decision <capabilities|status|patterns|validate|evaluate|setup> [options]',
7
+ 'Usage: aiwg decision <capabilities|status|patterns|validate|evaluate|setup|history|stats> [options]',
8
+ ' aiwg decision ask --question "<q>" (--yes-no | --choices a,b,c | --scale 1-5) [--context <text> | --context-file <path> | --context-stdin] [--threshold 0.8] [--timeout-ms 15000] [--provider <id>] [--session <id>] [--caller <tag>] [--record-text] [--json]',
8
9
  '',
9
10
  'Commands:',
10
11
  ' capabilities|status Show offline readiness, primitives, config and feature availability',
@@ -15,6 +16,34 @@ function usage() {
15
16
  ' validate <request|definition|ruleset|binding> <path>',
16
17
  ' evaluate --request <path> [--host-policy-module <path>]',
17
18
  ' setup synthetic-classification [--output-dir <dir>]',
19
+ ' setup jev [--token-stdin] [--region <r>] [--endpoint <url>] [--verify] [--remove]',
20
+ ' ask --question "<q>" (--yes-no | --choices a,b,c | --scale 1-5) [--provider <id>] [--session <id>] [--caller <tag>] [--record-text]',
21
+ ' history [--since <date>] [--provider <id>] [--session <id>] [--kind yes-no|choices|scale] [--status answered|fallback] [--caller <tag>|<prefix>* | --no-caller] [--json]',
22
+ ' stats [--since <date>] [--provider <id>] [--session <id>] [--kind yes-no|choices|scale] [--status answered|fallback] [--caller <tag>|<prefix>* | --no-caller] [--group-by provider|session|kind|caller] [--json]',
23
+ '',
24
+ 'Notes:',
25
+ ' Every COMPLETED `ask` call (CLI and MCP) appends one record to the decision',
26
+ ' ledger (.aiwg/decision/ledger.jsonl, resolved like `aiwg artifacts path`).',
27
+ ' Usage errors (bad flags, oversized context) are never recorded. Question',
28
+ ' and context text are never written unless --record-text is passed, and even',
29
+ ' then only after redaction and truncation to a documented size cap. Every',
30
+ ' other persisted field (session, caller, choice ids, answer, model, provider,',
31
+ ' project root) is also redacted before being written. `questionDigest` and',
32
+ ' `contextDigest` are plain SHA-256 over the raw text: a short, predictable',
33
+ ' question can still be matched by hashing candidate guesses, so a digest is',
34
+ ' not a confidentiality guarantee on its own.',
35
+ ' `history` and `stats` read that ledger offline — no network, no Jev call.',
36
+ ' `--since` is strict ISO-8601: a bare date or an offsetless timestamp is',
37
+ ' interpreted as UTC; invalid calendar dates (e.g. 2026-02-30) are rejected,',
38
+ ' including when an explicit offset is present.',
39
+ ` --session and --caller must match ${ATTRIBUTION_FIELD_PATTERN.source} (<= ${MAX_ATTRIBUTION_FIELD_LENGTH} characters).`,
40
+ ' `history`/`stats` accept `--caller <tag>` (exact), `--caller \'<prefix>*\'` (prefix; the only wildcard), or `--no-caller` (records with no tag; not combinable with `--caller`).',
41
+ ' `history`/`stats` reject unknown flags and positional arguments with exit 2.',
42
+ ' The ledger has no built-in retention: it grows without bound and the',
43
+ ' operator may truncate or delete it at any time; readers tolerate a missing',
44
+ ' file. The ledger trusts only the local filesystem under the project',
45
+ ' artifact root, owned by the same user running aiwg — it is not a defense',
46
+ ' against a hostile process already running as that user.',
18
47
  ].join('\n');
19
48
  }
20
49
  function takeOption(args, name) {
@@ -23,11 +52,266 @@ function takeOption(args, name) {
23
52
  return undefined;
24
53
  return args[index + 1];
25
54
  }
55
+ function takeRequiredOption(args, name) {
56
+ const value = takeOption(args, name);
57
+ if (!value || value.startsWith('--'))
58
+ throw new DecisionAskUsageError(`missing-${name.slice(2)}`, `${name} requires a value`);
59
+ return value;
60
+ }
26
61
  function patternId(value) {
27
62
  if (!value)
28
63
  throw new Error('Pattern id is required');
29
64
  return value;
30
65
  }
66
+ function flagCount(args, names) {
67
+ return args.filter(arg => names.includes(arg)).length;
68
+ }
69
+ async function readStdin() {
70
+ const chunks = [];
71
+ for await (const chunk of process.stdin)
72
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
73
+ return Buffer.concat(chunks).toString('utf8');
74
+ }
75
+ function parseNumber(value, name) {
76
+ if (value === undefined)
77
+ throw new DecisionAskUsageError(`missing-${name.slice(2)}`, `${name} requires a value`);
78
+ const parsed = Number(value);
79
+ if (!Number.isFinite(parsed))
80
+ throw new DecisionAskUsageError(`invalid-${name.slice(2)}`, `${name} must be numeric`);
81
+ return parsed;
82
+ }
83
+ function parseScale(value) {
84
+ if (!value)
85
+ throw new DecisionAskUsageError('invalid-scale', '--scale requires a range like 1-5');
86
+ const match = /^(-?\d+)-(-?\d+)$/.exec(value);
87
+ if (!match)
88
+ throw new DecisionAskUsageError('invalid-scale', '--scale requires a range like 1-5');
89
+ return { low: Number(match[1]), high: Number(match[2]) };
90
+ }
91
+ /** Shared by --session and --caller (review r1 finding 1): a conservative identifier shape, rejected with exit 2. */
92
+ function takeAttributionOption(args, name, reason) {
93
+ const value = takeOption(args, name);
94
+ if (value === undefined)
95
+ return undefined;
96
+ if (!ATTRIBUTION_FIELD_PATTERN.test(value)) {
97
+ throw new DecisionAskUsageError(reason, `${name} must match ${ATTRIBUTION_FIELD_PATTERN.source} (<= ${MAX_ATTRIBUTION_FIELD_LENGTH} characters)`);
98
+ }
99
+ return value;
100
+ }
101
+ async function decisionSetupJev(rest) {
102
+ const token = rest.includes('--token-stdin') ? (await readStdin()).trim() : undefined;
103
+ const result = await setupJev({
104
+ token,
105
+ region: takeOption(rest, '--region'),
106
+ endpoint: takeOption(rest, '--endpoint'),
107
+ verify: rest.includes('--verify'),
108
+ remove: rest.includes('--remove'),
109
+ }, { env: process.env });
110
+ return jsonResult(result);
111
+ }
112
+ async function decisionAsk(ctx) {
113
+ const args = ctx.args.slice(1);
114
+ const modeCount = flagCount(args, ['--yes-no']) + (args.includes('--choices') ? 1 : 0) + (args.includes('--scale') ? 1 : 0);
115
+ if (modeCount !== 1)
116
+ throw new DecisionAskUsageError('invalid-mode', 'Select exactly one of --yes-no, --choices, or --scale');
117
+ const contextModes = flagCount(args, ['--context', '--context-file', '--context-stdin']);
118
+ if (contextModes > 1)
119
+ throw new DecisionAskUsageError('invalid-context', 'Select at most one context source');
120
+ // Validated before any Jev call, so a malformed attribution flag never wastes one.
121
+ const explicitSessionId = takeAttributionOption(args, '--session', 'invalid-session');
122
+ const caller = takeAttributionOption(args, '--caller', 'invalid-caller');
123
+ const question = args.includes('--question') ? takeRequiredOption(args, '--question') : '';
124
+ let context = takeOption(args, '--context');
125
+ if (args.includes('--context') && (context === undefined || context.startsWith('--'))) {
126
+ throw new DecisionAskUsageError('missing-context', '--context requires text');
127
+ }
128
+ if (args.includes('--context-file')) {
129
+ const filePath = takeRequiredOption(args, '--context-file');
130
+ context = await readDecisionContextFile(filePath);
131
+ }
132
+ if (args.includes('--context-stdin'))
133
+ context = await readStdin();
134
+ const choices = args.includes('--choices') ? takeRequiredOption(args, '--choices') : undefined;
135
+ const scale = args.includes('--scale') ? parseScale(takeOption(args, '--scale')) : null;
136
+ const input = {
137
+ question,
138
+ mode: args.includes('--yes-no') ? { kind: 'yes-no' }
139
+ : choices !== undefined ? { kind: 'choices', choices: choices.split(',').map(value => value.trim()).filter(Boolean) }
140
+ : { kind: 'scale', ...scale },
141
+ ...(context === undefined ? {} : { context }),
142
+ ...(args.includes('--threshold') ? { threshold: parseNumber(takeOption(args, '--threshold'), '--threshold') } : {}),
143
+ ...(args.includes('--timeout-ms') ? { timeoutMs: parseNumber(takeOption(args, '--timeout-ms'), '--timeout-ms') } : {}),
144
+ };
145
+ const result = await askDecision(input, { env: process.env });
146
+ // Best-effort and bounded (recordDecisionAsk never throws and never hangs past its own
147
+ // deadline): a ledger failure, timeout, or attribution-detection error can never change
148
+ // `result` or the exit code decided below (review r1 findings 3 and 4).
149
+ await recordDecisionAsk(input, result, {
150
+ transport: 'cli',
151
+ projectRoot: ctx.cwd,
152
+ env: process.env,
153
+ explicitProvider: takeOption(args, '--provider'),
154
+ explicitSessionId,
155
+ caller: caller ?? null,
156
+ recordText: args.includes('--record-text'),
157
+ });
158
+ if (args.includes('--json'))
159
+ return jsonResult(result);
160
+ if (result.status === 'fallback')
161
+ return { exitCode: 0, message: `FALLBACK to LLM: ${result.reason}`, rawOutput: true };
162
+ return { exitCode: 0, message: `${String(result.answer)} (confidence ${result.confidence?.toFixed(2) ?? 'unknown'}, ${result.model ?? 'unknown-model'})`, rawOutput: true };
163
+ }
164
+ function parseSince(value) {
165
+ if (value === undefined)
166
+ return undefined;
167
+ try {
168
+ return parseDecisionLedgerSince(value);
169
+ }
170
+ catch (error) {
171
+ if (error instanceof DecisionLedgerSinceError)
172
+ throw new DecisionAskUsageError('invalid-since', error.message);
173
+ throw error;
174
+ }
175
+ }
176
+ const DECISION_LEDGER_KINDS = ['yes-no', 'choices', 'scale'];
177
+ const DECISION_LEDGER_STATUSES = ['answered', 'fallback'];
178
+ const DECISION_LEDGER_GROUP_BYS = ['provider', 'session', 'kind', 'caller'];
179
+ function parseKindFilter(value) {
180
+ if (value === undefined)
181
+ return undefined;
182
+ if (!DECISION_LEDGER_KINDS.includes(value)) {
183
+ throw new DecisionAskUsageError('invalid-kind', `--kind must be one of ${DECISION_LEDGER_KINDS.join(', ')}`);
184
+ }
185
+ return value;
186
+ }
187
+ function parseStatusFilter(value) {
188
+ if (value === undefined)
189
+ return undefined;
190
+ if (!DECISION_LEDGER_STATUSES.includes(value)) {
191
+ throw new DecisionAskUsageError('invalid-status', `--status must be one of ${DECISION_LEDGER_STATUSES.join(', ')}`);
192
+ }
193
+ return value;
194
+ }
195
+ function parseGroupBy(value) {
196
+ if (value === undefined)
197
+ return undefined;
198
+ if (!DECISION_LEDGER_GROUP_BYS.includes(value)) {
199
+ throw new DecisionAskUsageError('invalid-group-by', `--group-by must be one of ${DECISION_LEDGER_GROUP_BYS.join(', ')}`);
200
+ }
201
+ return value;
202
+ }
203
+ const HISTORY_VALUE_FLAGS = ['--since', '--provider', '--session', '--kind', '--status', '--caller'];
204
+ const HISTORY_BOOL_FLAGS = ['--json', '--no-caller'];
205
+ const STATS_VALUE_FLAGS = [...HISTORY_VALUE_FLAGS, '--group-by'];
206
+ const STATS_BOOL_FLAGS = HISTORY_BOOL_FLAGS;
207
+ /**
208
+ * Strict allow-list for `history`/`stats`: unknown `--flags` and positional
209
+ * arguments are usage errors (exit 2) naming the offending token, instead of
210
+ * being silently ignored. `ask`/`setup` parsing is intentionally untouched.
211
+ */
212
+ function assertLedgerArgs(args, valueFlags, boolFlags) {
213
+ let index = 0;
214
+ while (index < args.length) {
215
+ const token = args[index];
216
+ if (valueFlags.includes(token)) {
217
+ if (args[index + 1] === undefined)
218
+ throw new DecisionAskUsageError(`missing-${token.slice(2)}`, `${token} requires a value`);
219
+ index += 2;
220
+ continue;
221
+ }
222
+ if (boolFlags.includes(token)) {
223
+ index += 1;
224
+ continue;
225
+ }
226
+ if (token.startsWith('--'))
227
+ throw new DecisionAskUsageError('unknown-flag', `unknown flag ${token}`);
228
+ throw new DecisionAskUsageError('unexpected-argument', `unexpected argument ${token}`);
229
+ }
230
+ }
231
+ /**
232
+ * Parses `--caller <tag>` for `history`/`stats`: an exact match, except a
233
+ * single trailing `*` means prefix match (`--caller 'address-issues:*'`).
234
+ * The tag itself must match the attribution shape; `--no-caller` (records
235
+ * with no caller tag) is parsed separately and cannot be combined with it.
236
+ */
237
+ function parseCallerFilter(args) {
238
+ const raw = takeOption(args, '--caller');
239
+ const noCaller = args.includes('--no-caller');
240
+ if (raw !== undefined && noCaller) {
241
+ throw new DecisionAskUsageError('invalid-caller-filter', '--caller and --no-caller cannot be used together');
242
+ }
243
+ if (noCaller)
244
+ return { noCaller: true };
245
+ if (raw === undefined)
246
+ return {};
247
+ if (raw === '' || raw.startsWith('--')) {
248
+ throw new DecisionAskUsageError('invalid-caller', '--caller requires a non-empty value');
249
+ }
250
+ const prefix = raw.endsWith('*');
251
+ const tag = prefix ? raw.slice(0, -1) : raw;
252
+ if (tag === '' || !ATTRIBUTION_FIELD_PATTERN.test(tag)) {
253
+ throw new DecisionAskUsageError('invalid-caller', `--caller must match ${ATTRIBUTION_FIELD_PATTERN.source} (<= ${MAX_ATTRIBUTION_FIELD_LENGTH} characters), allowing one trailing *`);
254
+ }
255
+ return prefix ? { callerPrefix: tag } : { caller: tag };
256
+ }
257
+ /** Shared by `history` and `stats` (review r1 finding 9): the same filters, parsed the same way. */
258
+ function parseLedgerFilters(args) {
259
+ return {
260
+ since: parseSince(takeOption(args, '--since')),
261
+ provider: takeOption(args, '--provider'),
262
+ sessionId: takeOption(args, '--session'),
263
+ kind: parseKindFilter(takeOption(args, '--kind')),
264
+ status: parseStatusFilter(takeOption(args, '--status')),
265
+ ...parseCallerFilter(args),
266
+ };
267
+ }
268
+ /** One header line stating the caller filter, shown only when one is set. */
269
+ function callerFilterHeader(filters) {
270
+ if (filters.noCaller)
271
+ return 'Caller filter: no caller (records with no caller tag)';
272
+ if (filters.caller !== undefined)
273
+ return `Caller filter: ${filters.caller}`;
274
+ if (filters.callerPrefix !== undefined)
275
+ return `Caller filter: ${filters.callerPrefix}*`;
276
+ return null;
277
+ }
278
+ async function decisionHistory(ctx) {
279
+ const args = ctx.args.slice(1);
280
+ assertLedgerArgs(args, HISTORY_VALUE_FLAGS, HISTORY_BOOL_FLAGS);
281
+ const filters = parseLedgerFilters(args);
282
+ const result = await decisionLedgerHistory(ctx.cwd, filters);
283
+ if (args.includes('--json'))
284
+ return jsonResult(result);
285
+ const header = result.skipped > 0 ? [`(${result.skipped} invalid line(s) skipped)`] : [];
286
+ const callerHeader = callerFilterHeader(filters);
287
+ if (callerHeader)
288
+ header.push(callerHeader);
289
+ if (result.count === 0)
290
+ return { exitCode: 0, message: [...header, 'No decision ledger records match the filters.'].join('\n'), rawOutput: true };
291
+ const lines = result.records.map(record => `${record.timestamp} ${record.status.padEnd(8)} provider=${record.provider} kind=${record.decisionKind} confidence=${record.confidence ?? '-'} caller=${record.caller ?? '-'} id=${record.id}`);
292
+ return { exitCode: 0, message: [...header, ...lines].join('\n'), rawOutput: true };
293
+ }
294
+ async function decisionStats(ctx) {
295
+ const args = ctx.args.slice(1);
296
+ assertLedgerArgs(args, STATS_VALUE_FLAGS, STATS_BOOL_FLAGS);
297
+ const filters = { ...parseLedgerFilters(args), groupBy: parseGroupBy(takeOption(args, '--group-by')) };
298
+ const result = await decisionLedgerStats(ctx.cwd, filters);
299
+ if (args.includes('--json'))
300
+ return jsonResult(result);
301
+ const header = result.skipped > 0 ? [`(${result.skipped} invalid line(s) skipped)`] : [];
302
+ const callerHeader = callerFilterHeader(filters);
303
+ if (callerHeader)
304
+ header.push(callerHeader);
305
+ if (result.groups.length === 0)
306
+ return { exitCode: 0, message: [...header, 'No decision ledger records match the filters.'].join('\n'), rawOutput: true };
307
+ const lines = result.groups.flatMap(group => [
308
+ `${result.groupBy ?? 'all'}=${group.key ?? 'null'}: ${group.totalCalls} calls (${group.answered} answered, ${group.fallback} fallback, rate=${group.fallbackRate.toFixed(2)})`,
309
+ ` confidence: mean=${group.confidence.mean?.toFixed(2) ?? '-'} min=${group.confidence.min?.toFixed(2) ?? '-'} max=${group.confidence.max?.toFixed(2) ?? '-'} (n=${group.confidence.count})`,
310
+ ` jev usage: input=${group.jevUsage.inputTokens} output=${group.jevUsage.outputTokens}`,
311
+ ` estimated LLM tokens avoided=${group.estimatedLlmTokensAvoidedTotal} net savings=${group.netSavingsTokens}`,
312
+ ]);
313
+ return { exitCode: 0, message: [...header, ...lines].join('\n'), rawOutput: true };
314
+ }
31
315
  async function executeDecision(ctx) {
32
316
  const [command, subcommand, ...rest] = ctx.args;
33
317
  const options = { cwd: ctx.cwd, frameworkRoot: ctx.frameworkRoot, env: process.env };
@@ -36,6 +320,12 @@ async function executeDecision(ctx) {
36
320
  return { exitCode: 0, message: usage(), rawOutput: true };
37
321
  if (command === 'capabilities' || command === 'status')
38
322
  return jsonResult(decisionCapabilities(options));
323
+ if (command === 'ask')
324
+ return await decisionAsk(ctx);
325
+ if (command === 'history')
326
+ return await decisionHistory(ctx);
327
+ if (command === 'stats')
328
+ return await decisionStats(ctx);
39
329
  if (command === 'patterns') {
40
330
  if (subcommand === 'list')
41
331
  return jsonResult(listPatterns());
@@ -70,9 +360,14 @@ async function executeDecision(ctx) {
70
360
  return jsonResult(await materializeSyntheticClassificationSetup(outputDir, {}, options));
71
361
  return jsonResult(syntheticClassificationSetup({}, options));
72
362
  }
363
+ if (command === 'setup' && subcommand === 'jev')
364
+ return await decisionSetupJev(rest);
73
365
  return { exitCode: 2, message: usage(), rawOutput: true };
74
366
  }
75
367
  catch (error) {
368
+ if (error instanceof DecisionAskUsageError) {
369
+ return { exitCode: 2, message: JSON.stringify({ schema: 'aiwg-decision-error/v1', reason: error.reason, message: error.message }, null, 2), rawOutput: true };
370
+ }
76
371
  return { exitCode: 1, message: error instanceof Error ? error.message : String(error) };
77
372
  }
78
373
  }