openplanr 1.17.0 → 1.18.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.
- package/dist/cli/commands/operate.d.ts +10 -1
- package/dist/cli/commands/operate.d.ts.map +1 -1
- package/dist/cli/commands/operate.js +55 -7
- package/dist/cli/commands/operate.js.map +1 -1
- package/dist/services/ai-service.d.ts +25 -0
- package/dist/services/ai-service.d.ts.map +1 -1
- package/dist/services/ai-service.js +38 -0
- package/dist/services/ai-service.js.map +1 -1
- package/dist/services/operate/advisors.d.ts +37 -1
- package/dist/services/operate/advisors.d.ts.map +1 -1
- package/dist/services/operate/advisors.js +351 -10
- package/dist/services/operate/advisors.js.map +1 -1
- package/dist/services/operate/config.d.ts +26 -0
- package/dist/services/operate/config.d.ts.map +1 -1
- package/dist/services/operate/config.js +41 -0
- package/dist/services/operate/config.js.map +1 -1
- package/dist/services/operate/doctor.d.ts.map +1 -1
- package/dist/services/operate/doctor.js +121 -1
- package/dist/services/operate/doctor.js.map +1 -1
- package/dist/services/operate/evidence.d.ts.map +1 -1
- package/dist/services/operate/evidence.js +29 -0
- package/dist/services/operate/evidence.js.map +1 -1
- package/dist/services/operate/index.d.ts +2 -0
- package/dist/services/operate/index.d.ts.map +1 -1
- package/dist/services/operate/index.js +237 -41
- package/dist/services/operate/index.js.map +1 -1
- package/dist/services/operate/interaction/answer-service.d.ts +17 -0
- package/dist/services/operate/interaction/answer-service.d.ts.map +1 -1
- package/dist/services/operate/interaction/answer-service.js +49 -7
- package/dist/services/operate/interaction/answer-service.js.map +1 -1
- package/dist/services/operate/interaction/question-engine.d.ts.map +1 -1
- package/dist/services/operate/interaction/question-engine.js +9 -3
- package/dist/services/operate/interaction/question-engine.js.map +1 -1
- package/dist/services/operate/interaction/question-registry.d.ts +13 -0
- package/dist/services/operate/interaction/question-registry.d.ts.map +1 -1
- package/dist/services/operate/interaction/question-registry.js +116 -26
- package/dist/services/operate/interaction/question-registry.js.map +1 -1
- package/dist/services/operate/interaction/terminal-renderer.d.ts +12 -1
- package/dist/services/operate/interaction/terminal-renderer.d.ts.map +1 -1
- package/dist/services/operate/interaction/terminal-renderer.js +26 -15
- package/dist/services/operate/interaction/terminal-renderer.js.map +1 -1
- package/dist/services/operate/lifecycle.d.ts +8 -0
- package/dist/services/operate/lifecycle.d.ts.map +1 -1
- package/dist/services/operate/lifecycle.js +19 -1
- package/dist/services/operate/lifecycle.js.map +1 -1
- package/dist/services/operate/maintenance.d.ts +22 -0
- package/dist/services/operate/maintenance.d.ts.map +1 -1
- package/dist/services/operate/maintenance.js +397 -44
- package/dist/services/operate/maintenance.js.map +1 -1
- package/dist/services/operate/protocol.d.ts +4 -1
- package/dist/services/operate/protocol.d.ts.map +1 -1
- package/dist/services/operate/protocol.js.map +1 -1
- package/dist/services/operate/reports.d.ts +9 -0
- package/dist/services/operate/reports.d.ts.map +1 -1
- package/dist/services/operate/reports.js +9 -0
- package/dist/services/operate/reports.js.map +1 -1
- package/dist/services/operate/types.d.ts +19 -0
- package/dist/services/operate/types.d.ts.map +1 -1
- package/dist/services/operate/types.js.map +1 -1
- package/dist/services/runtime-manager-service.d.ts.map +1 -1
- package/dist/services/runtime-manager-service.js +25 -5
- package/dist/services/runtime-manager-service.js.map +1 -1
- package/package.json +2 -2
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { resolveAIProviderReadiness } from '../ai-service.js';
|
|
4
|
+
import { loadConfig } from '../config-service.js';
|
|
3
5
|
import { computeNextDueAt } from './cadence.js';
|
|
4
6
|
import { canonicalDigest, sha256Digest } from './canonical.js';
|
|
5
7
|
import { applyOperatingInitialization, getOperatingProfile, listOperatingProfiles, normalizeCustomOperatingProfile, normalizeOperatingInitializationAnswers, parseOperatingDispatchModeOverrideFlags, prepareOperatingInitialization, readOperatingLastRunAt, validateOperatingConfiguration, } from './config.js';
|
|
@@ -9,7 +11,7 @@ import { classifyEvidenceDiagnostic } from './evidence-classifications.js';
|
|
|
9
11
|
import { listEvidenceDiagnostics, readEvidenceDiagnostic } from './evidence-diagnostics.js';
|
|
10
12
|
import { parseStrictJson, readImportedEvidenceFile } from './evidence-import.js';
|
|
11
13
|
import { createOperatingAction } from './interaction/action-service.js';
|
|
12
|
-
import { persistableOperatingInitAnswers, resumeGuidedSession, submitGuidedAnswers, } from './interaction/answer-service.js';
|
|
14
|
+
import { persistableOperatingInitAnswers, probeAvailableEvidenceSources, probeGitUserName, probePipelineInstalled, resumeGuidedSession, submitGuidedAnswers, } from './interaction/answer-service.js';
|
|
13
15
|
import { assertOperatingConfirmation } from './interaction/confirmation-service.js';
|
|
14
16
|
import { decodeOperatingInitializationReplay, encodeOperatingInitializationReplay, } from './interaction/initialization-replay.js';
|
|
15
17
|
import { createOperatingInitQuestionnaire, evaluateOperatingInitQuestions, operatingInitAnswersFromOptions, } from './interaction/question-engine.js';
|
|
@@ -17,7 +19,7 @@ import { cancelGuidedSession, createGuidedSession, createGuidedSessionId, curren
|
|
|
17
19
|
import { assertCommittedOperatingView } from './journal.js';
|
|
18
20
|
import { applyOperatingMigration, inspectOperatingMigration, rollbackOperatingMigration, } from './legacy-import-service.js';
|
|
19
21
|
import { answerOperatingGap, applyOrRollbackRoute, decideOperatingDecision, governOperatingFinding, readOperatingCollection, readOperatingReview, transitionOperatingCycle, verifyOperatingGap, } from './lifecycle.js';
|
|
20
|
-
import { createOperatingAdapterStartHandoff, exportOperatingDiagnostics, operateAdapterLifecycle, operatingCacheAction, operatingIntegrityAction, repairOperatingSecurity, } from './maintenance.js';
|
|
22
|
+
import { createOperatingAdapterStartHandoff, exportOperatingDiagnostics, operateAdapterLifecycle, operatingCacheAction, operatingIntegrityAction, purgeBoardMachineLocalCaches, repairOperatingSecurity, } from './maintenance.js';
|
|
21
23
|
import { migrateOperatingStorageLayoutOnOpen } from './migration.js';
|
|
22
24
|
import { renderOperatingBrief } from './projection.js';
|
|
23
25
|
import { loadOperatingProtocol, resolveOperatingPipelineRoot } from './protocol.js';
|
|
@@ -50,17 +52,43 @@ function stringList(value) {
|
|
|
50
52
|
.filter(Boolean);
|
|
51
53
|
return [];
|
|
52
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Probe the coding-runtime identity of the host process from the environment
|
|
57
|
+
* markers set by the agent that launched the CLI. Mirrors the compatible-runtime
|
|
58
|
+
* detection in terminal-renderer.ts's `detectOperatingQuestionContext`, but keys
|
|
59
|
+
* off the launcher's env markers so a non-interactive/JSON invocation can stamp a
|
|
60
|
+
* truthful adapter block and resolve `auto` instead of stamping `unknown`/`none`
|
|
61
|
+
* or silently disabling native dispatch.
|
|
62
|
+
*/
|
|
63
|
+
function detectOperatingHostRuntime() {
|
|
64
|
+
const env = process.env;
|
|
65
|
+
const marker = (value) => (value ?? '').length > 0;
|
|
66
|
+
if (marker(env.CLAUDECODE) || marker(env.CLAUDE_CODE_ENTRYPOINT))
|
|
67
|
+
return 'claude';
|
|
68
|
+
if (marker(env.CURSOR_TRACE_ID) || marker(env.CURSOR_AGENT))
|
|
69
|
+
return 'cursor';
|
|
70
|
+
if (marker(env.CODEX_SANDBOX) || marker(env.CODEX_HOME))
|
|
71
|
+
return 'codex';
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
53
74
|
async function resolvedOperatingRuntime(projectRoot, requested) {
|
|
54
75
|
if (requested !== 'auto')
|
|
55
76
|
return requested;
|
|
56
|
-
|
|
77
|
+
const persisted = await readFile(path.join(resolveOperatingPaths(projectRoot).localRoot, 'preferences.json'), 'utf8')
|
|
57
78
|
.then((raw) => {
|
|
58
79
|
const runtime = JSON.parse(raw).runtime;
|
|
59
80
|
return typeof runtime === 'string' && runtime ? runtime : 'auto';
|
|
60
81
|
})
|
|
61
82
|
.catch(() => 'auto');
|
|
83
|
+
if (persisted !== 'auto')
|
|
84
|
+
return persisted;
|
|
85
|
+
// A persisted (or absent) `auto` preference must never silently disable native
|
|
86
|
+
// dispatch inside a capable host: resolve it to the detected runtime identity
|
|
87
|
+
// so `usesNativeOperatingAdvisors` evaluates the real host, not the `auto`
|
|
88
|
+
// placeholder that always returns false.
|
|
89
|
+
return detectOperatingHostRuntime() ?? 'auto';
|
|
62
90
|
}
|
|
63
|
-
async function usesNativeOperatingAdvisors(projectRoot, requestedRuntime) {
|
|
91
|
+
export async function usesNativeOperatingAdvisors(projectRoot, requestedRuntime) {
|
|
64
92
|
const runtime = await resolvedOperatingRuntime(projectRoot, requestedRuntime);
|
|
65
93
|
const adapterId = runtime === 'claude' ? 'claude-code' : runtime;
|
|
66
94
|
if (adapterId === 'auto')
|
|
@@ -74,7 +102,10 @@ async function usesNativeOperatingAdvisors(projectRoot, requestedRuntime) {
|
|
|
74
102
|
const adapter = registry.adapters?.find((entry) => entry.id === adapterId);
|
|
75
103
|
if (adapter?.capabilities?.operatingBoard !== true)
|
|
76
104
|
return false;
|
|
77
|
-
if (
|
|
105
|
+
if (
|
|
106
|
+
// `native-read-only` is the US-001/T-001 adapters-registry capability name that
|
|
107
|
+
// supersedes the earlier isolation labels; recognize it alongside them.
|
|
108
|
+
['native-isolated', 'native-bounded', 'native-read-only'].includes(adapter.capabilities.operatingAdvisorDispatch ?? '')) {
|
|
78
109
|
return true;
|
|
79
110
|
}
|
|
80
111
|
// Compatibility with Protocol v1.2 registries published before the
|
|
@@ -123,6 +154,47 @@ function success(action, value = {}) {
|
|
|
123
154
|
nextActions,
|
|
124
155
|
};
|
|
125
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* A healthy continuation (FR7/E-007): a guided-stage advance
|
|
159
|
+
* (`E_OPERATE_INPUT_REQUIRED`) or first-use provider consent
|
|
160
|
+
* (`E_OPERATE_AUTHORITY_REQUIRED`) is not a failure. It is returned as an
|
|
161
|
+
* `ok: true` handoff carrying a machine-readable `flow: 'handoff'` discriminator
|
|
162
|
+
* (and the originating `code`), mirroring `run`'s adapter-handoff shape so a
|
|
163
|
+
* harness reads the pause without treating it as a red exit. No `exitCode` is
|
|
164
|
+
* set: the CLI leaves the process exit code at 0 for `ok: true` results.
|
|
165
|
+
*/
|
|
166
|
+
function handoffContinuation(action, code, value = {}) {
|
|
167
|
+
return { ...success(action, value), code, flow: 'handoff' };
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* First-use / renewal provider consent is disclosed by
|
|
171
|
+
* `ensureOperatingProviderConsent` (advisors.ts) as an
|
|
172
|
+
* `E_OPERATE_AUTHORITY_REQUIRED` carrying the full policy disclosure
|
|
173
|
+
* (`endpoint`, `permittedDataClasses`, `policyDigest`). That specific
|
|
174
|
+
* disclosure is a continuation, not a refusal — unlike every other
|
|
175
|
+
* `E_OPERATE_AUTHORITY_REQUIRED` (a mutation attempted without `--yes`), which
|
|
176
|
+
* stays an `ok: false` exit-4 failure so the authority model is unchanged.
|
|
177
|
+
*/
|
|
178
|
+
function isProviderConsentHandoff(error) {
|
|
179
|
+
if (!(error instanceof OperateError) || error.code !== 'E_OPERATE_AUTHORITY_REQUIRED') {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
const details = error.details;
|
|
183
|
+
return (typeof details === 'object' &&
|
|
184
|
+
details !== null &&
|
|
185
|
+
'policyDigest' in details &&
|
|
186
|
+
'endpoint' in details &&
|
|
187
|
+
'permittedDataClasses' in details);
|
|
188
|
+
}
|
|
189
|
+
function providerConsentContinuation(action, error) {
|
|
190
|
+
const retry = `planr operate ${action} --yes`;
|
|
191
|
+
return handoffContinuation(action, 'E_OPERATE_AUTHORITY_REQUIRED', {
|
|
192
|
+
message: error.message,
|
|
193
|
+
data: error.details,
|
|
194
|
+
nextActions: [retry],
|
|
195
|
+
next: [retry],
|
|
196
|
+
});
|
|
197
|
+
}
|
|
126
198
|
/**
|
|
127
199
|
* Stable process exit classes for automation.
|
|
128
200
|
*
|
|
@@ -135,6 +207,15 @@ function success(action, value = {}) {
|
|
|
135
207
|
*
|
|
136
208
|
* Keep this exhaustive so adding a public Operate error cannot silently fall
|
|
137
209
|
* through to the internal-error class.
|
|
210
|
+
*
|
|
211
|
+
* FR7/E-007 continuation note: `E_OPERATE_INPUT_REQUIRED` and
|
|
212
|
+
* `E_OPERATE_AUTHORITY_REQUIRED` keep this class-4 mapping for the cases that
|
|
213
|
+
* are genuine refusals — a mutation attempted without `--yes` still fails with
|
|
214
|
+
* `ok: false` and exit 4. But a *healthy continuation* — a guided-stage advance
|
|
215
|
+
* or first-use provider consent — is not a failure: it is returned as an
|
|
216
|
+
* `ok: true` handoff (`flow: 'handoff'`) with no failure exit code, mirroring
|
|
217
|
+
* `run`'s adapter handoff, so a harness never paints the happy path red. The
|
|
218
|
+
* numeric class below is therefore only consulted for the failure branch.
|
|
138
219
|
*/
|
|
139
220
|
const OPERATE_EXIT_CODES = {
|
|
140
221
|
E_OPERATE_INTERNAL: 1,
|
|
@@ -187,8 +268,16 @@ const OPERATE_EXIT_CODES = {
|
|
|
187
268
|
function operateExitCode(code) {
|
|
188
269
|
return OPERATE_EXIT_CODES[code];
|
|
189
270
|
}
|
|
190
|
-
function failure(action, error) {
|
|
271
|
+
export function failure(action, error) {
|
|
191
272
|
const code = error instanceof OperateError ? error.code : 'E_OPERATE_INTERNAL';
|
|
273
|
+
// E_OPERATE_INTERNAL must never be zero-information: record the redacted error
|
|
274
|
+
// class/name (no message or stack, which can carry paths or secrets) so the
|
|
275
|
+
// diagnostics export and automation callers can classify the internal failure.
|
|
276
|
+
const internalErrorClass = code === 'E_OPERATE_INTERNAL'
|
|
277
|
+
? error instanceof Error && typeof error.name === 'string' && error.name.length > 0
|
|
278
|
+
? error.name
|
|
279
|
+
: 'Error'
|
|
280
|
+
: undefined;
|
|
192
281
|
const confirmationAction = error instanceof OperateError &&
|
|
193
282
|
error.details?.action &&
|
|
194
283
|
typeof error.details.action === 'object' &&
|
|
@@ -248,11 +337,29 @@ function failure(action, error) {
|
|
|
248
337
|
counts: {},
|
|
249
338
|
warnings: [],
|
|
250
339
|
nextActions,
|
|
251
|
-
data: error instanceof OperateError
|
|
340
|
+
data: error instanceof OperateError
|
|
341
|
+
? error.details
|
|
342
|
+
: internalErrorClass
|
|
343
|
+
? { errorClass: internalErrorClass }
|
|
344
|
+
: undefined,
|
|
252
345
|
next: nextActions,
|
|
253
346
|
exitCode: operateExitCode(code),
|
|
254
347
|
};
|
|
255
348
|
}
|
|
349
|
+
/**
|
|
350
|
+
* Reduce recovery `nextActions` to the set of public `planr operate` commands
|
|
351
|
+
* that back the structured actions. The digest-bound authority flags
|
|
352
|
+
* (`--yes`, `--confirm <digest>`, `--preview-digest <digest>`) are stripped from
|
|
353
|
+
* the *command* string — a raw sha256 digest must never enter a structured
|
|
354
|
+
* command (it would trip the sensitive-data guard in `assertSafeCommand`).
|
|
355
|
+
*
|
|
356
|
+
* FR8/E-008: stripping them here no longer strands a runner. For a
|
|
357
|
+
* digest-confirmable command the exact, ready-to-run argv (including the real
|
|
358
|
+
* `--confirm <digest> --yes` token) is re-surfaced on the structured action as
|
|
359
|
+
* `confirmArgv` in `attachStructuredActions`, so the runner never has to
|
|
360
|
+
* re-synthesize a confirmation token it was handed. A command whose only
|
|
361
|
+
* authority is `--yes` is never given a confirmationDigest at all.
|
|
362
|
+
*/
|
|
256
363
|
function publicActionCommands(nextActions) {
|
|
257
364
|
return [
|
|
258
365
|
...new Set(nextActions
|
|
@@ -265,6 +372,17 @@ function publicActionCommands(nextActions) {
|
|
|
265
372
|
.filter((value) => /^planr\s+/.test(value))),
|
|
266
373
|
];
|
|
267
374
|
}
|
|
375
|
+
/**
|
|
376
|
+
* The digest-bound confirmation flag a public command's CLI actually accepts, or
|
|
377
|
+
* `null` when its only authority is `--yes`. A confirmationDigest is meaningful
|
|
378
|
+
* only for a command that can consume it via `--confirm`; a `--yes`-only command
|
|
379
|
+
* (e.g. `operate run`) must never be handed one (FR8/E-008).
|
|
380
|
+
*/
|
|
381
|
+
function commandConfirmFlag(command) {
|
|
382
|
+
return /\boperate\s+init\b/.test(command) || /\bevidence\s+classify\b/.test(command)
|
|
383
|
+
? '--confirm'
|
|
384
|
+
: null;
|
|
385
|
+
}
|
|
268
386
|
function hasFlag(command, flag) {
|
|
269
387
|
return new RegExp(`(?:^|\\s)${flag.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?:\\s|$)`).test(command);
|
|
270
388
|
}
|
|
@@ -300,8 +418,9 @@ async function actionEffect(request, command) {
|
|
|
300
418
|
return 'project-write';
|
|
301
419
|
}
|
|
302
420
|
async function attachStructuredActions(request, result) {
|
|
303
|
-
if (result.actions?.length || result.action === 'input_required')
|
|
421
|
+
if (result.actions?.length || result.action === 'input_required' || result.flow === 'handoff') {
|
|
304
422
|
return result;
|
|
423
|
+
}
|
|
305
424
|
const commands = publicActionCommands(result.nextActions);
|
|
306
425
|
if (commands.length === 0)
|
|
307
426
|
return result;
|
|
@@ -327,6 +446,7 @@ async function attachStructuredActions(request, result) {
|
|
|
327
446
|
const actions = [];
|
|
328
447
|
for (const [index, command] of commands.entries()) {
|
|
329
448
|
const effect = await actionEffect(request, command);
|
|
449
|
+
const confirmFlag = commandConfirmFlag(command);
|
|
330
450
|
const id = `operate.next.${canonicalDigest({ command }).slice('sha256:'.length, 'sha256:'.length + 20)}`.toLowerCase();
|
|
331
451
|
const created = await createOperatingAction({
|
|
332
452
|
id,
|
|
@@ -356,8 +476,29 @@ async function attachStructuredActions(request, result) {
|
|
|
356
476
|
},
|
|
357
477
|
}),
|
|
358
478
|
});
|
|
359
|
-
|
|
479
|
+
let action = created.action;
|
|
480
|
+
// FR8/E-008: `operate run` authorizes with `--yes` alone — its CLI accepts no
|
|
481
|
+
// `--confirm` flag — so it must never carry a confirmationDigest a runner
|
|
482
|
+
// could never pass. It still appears as a structured action (mirroring the
|
|
483
|
+
// handoff `run` continuation) but with its digest binding cleared.
|
|
484
|
+
if (/\boperate\s+run\b/.test(command)) {
|
|
485
|
+
action = {
|
|
486
|
+
...action,
|
|
487
|
+
requiresConfirmation: false,
|
|
488
|
+
confirmationScope: null,
|
|
489
|
+
confirmationDigest: null,
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
// A digest-confirmable action (`--confirm <digest>`) carries its exact,
|
|
493
|
+
// ready-to-run argv so a runner never has to re-synthesize the confirmation
|
|
494
|
+
// token it was already handed.
|
|
495
|
+
const confirmArgv = confirmFlag && action.confirmationDigest
|
|
496
|
+
? [...command.split(/\s+/), confirmFlag, action.confirmationDigest, '--yes']
|
|
497
|
+
: undefined;
|
|
498
|
+
actions.push(confirmArgv ? { ...action, confirmArgv } : action);
|
|
360
499
|
}
|
|
500
|
+
if (actions.length === 0)
|
|
501
|
+
return result;
|
|
361
502
|
return { ...result, actions };
|
|
362
503
|
}
|
|
363
504
|
async function inspect(request) {
|
|
@@ -563,22 +704,14 @@ async function initialize(request) {
|
|
|
563
704
|
});
|
|
564
705
|
supplied = normalizeOperatingInitializationAnswers(resumedSession.answers);
|
|
565
706
|
if (resumedSession.status === 'input-required') {
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
action: 'input_required',
|
|
571
|
-
code: 'E_OPERATE_INPUT_REQUIRED',
|
|
707
|
+
// FR7/E-007: a guided-stage advance is a healthy continuation, not a
|
|
708
|
+
// failure. Report it as an `ok: true` handoff (`flow: 'handoff'`) carrying
|
|
709
|
+
// the next questionnaire, mirroring `run`'s adapter handoff.
|
|
710
|
+
return handoffContinuation('input_required', 'E_OPERATE_INPUT_REQUIRED', {
|
|
572
711
|
message: 'Operating Board initialization needs explicit human input.',
|
|
573
712
|
state: resumedSession.session.state,
|
|
574
|
-
paths: {},
|
|
575
|
-
counts: {},
|
|
576
|
-
warnings: [],
|
|
577
|
-
nextActions: [],
|
|
578
|
-
next: [],
|
|
579
713
|
questionnaire: resumedSession.questionnaire,
|
|
580
|
-
|
|
581
|
-
};
|
|
714
|
+
});
|
|
582
715
|
}
|
|
583
716
|
}
|
|
584
717
|
let customProfile;
|
|
@@ -590,13 +723,36 @@ async function initialize(request) {
|
|
|
590
723
|
supplied.profileFile) {
|
|
591
724
|
throw new OperateError('E_OPERATE_CONFIG_INVALID', '--profile-file is valid only with --profile custom.');
|
|
592
725
|
}
|
|
726
|
+
// Probe the real host runtime instead of stamping `unknown`/`none`: an explicit
|
|
727
|
+
// --runtime flag wins, otherwise the launcher's env markers name the host so the
|
|
728
|
+
// questionnaire's adapter block is truthful (and the runtime question can be a
|
|
729
|
+
// detect-don't-ask suggestion rather than a required prompt).
|
|
730
|
+
const detectedHostRuntime = detectOperatingHostRuntime();
|
|
731
|
+
const requestedRuntimeOption = option(request, 'runtime', 'auto');
|
|
732
|
+
// Probe the same signals the terminal path does so the JSON/native init path is
|
|
733
|
+
// equally truthful: the decision-owner suggestion reaches this path (gitUserName),
|
|
734
|
+
// the "locally available" source claim is real (availableSources), and the
|
|
735
|
+
// planning-engine detects pipeline-po when a compatible pipeline is installed.
|
|
736
|
+
const [gitUserName, availableSources] = await Promise.all([
|
|
737
|
+
probeGitUserName(request.projectRoot),
|
|
738
|
+
probeAvailableEvidenceSources(request.projectRoot),
|
|
739
|
+
]);
|
|
593
740
|
const context = {
|
|
594
741
|
projectRoot: request.projectRoot,
|
|
595
742
|
...bindings,
|
|
743
|
+
...(detectedHostRuntime ? { detectedRuntime: detectedHostRuntime } : {}),
|
|
744
|
+
...(gitUserName ? { gitUserName } : {}),
|
|
596
745
|
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
597
|
-
availableSources
|
|
598
|
-
|
|
599
|
-
|
|
746
|
+
availableSources,
|
|
747
|
+
pipelineInstalled: probePipelineInstalled(),
|
|
748
|
+
runtime: requestedRuntimeOption && requestedRuntimeOption !== 'auto'
|
|
749
|
+
? requestedRuntimeOption
|
|
750
|
+
: (detectedHostRuntime ?? (request.interactive ? 'terminal' : 'unknown')),
|
|
751
|
+
interaction: detectedHostRuntime
|
|
752
|
+
? 'native'
|
|
753
|
+
: request.interactive
|
|
754
|
+
? 'terminal'
|
|
755
|
+
: 'none',
|
|
600
756
|
};
|
|
601
757
|
const questionState = await evaluateOperatingInitQuestions({
|
|
602
758
|
answers: supplied,
|
|
@@ -617,22 +773,12 @@ async function initialize(request) {
|
|
|
617
773
|
persistedAnswers: persistableOperatingInitAnswers(questionState.answers, context),
|
|
618
774
|
localRoot,
|
|
619
775
|
});
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
action: 'input_required',
|
|
625
|
-
code: 'E_OPERATE_INPUT_REQUIRED',
|
|
626
|
-
message: 'Operating Board initialization needs explicit human input.',
|
|
627
|
-
state: null,
|
|
628
|
-
paths: {},
|
|
629
|
-
counts: {},
|
|
630
|
-
warnings: [],
|
|
631
|
-
nextActions: [],
|
|
632
|
-
next: [],
|
|
776
|
+
// FR7/E-007: the first guided-stage prompt is a healthy continuation — an
|
|
777
|
+
// `ok: true` handoff (`flow: 'handoff'`) carrying the questionnaire, not an
|
|
778
|
+
// exit-4 failure.
|
|
779
|
+
return handoffContinuation('input_required', 'E_OPERATE_INPUT_REQUIRED', {
|
|
633
780
|
questionnaire,
|
|
634
|
-
|
|
635
|
-
};
|
|
781
|
+
});
|
|
636
782
|
}
|
|
637
783
|
const profile = supplied.profile;
|
|
638
784
|
const decisionOwner = supplied.decisionOwner ?? option(request, 'decisionOwner', '').trim();
|
|
@@ -755,6 +901,11 @@ async function initialize(request) {
|
|
|
755
901
|
preview,
|
|
756
902
|
confirmationDigest: preview.previewDigest,
|
|
757
903
|
});
|
|
904
|
+
// FR4: a committed init apply is a fresh (re-genesised) board. Purge the
|
|
905
|
+
// machine-local advisor sessions and incremental evidence baselines a prior
|
|
906
|
+
// generation left at this path so the new board never inherits a stale
|
|
907
|
+
// session or a baseline bound to a superseded workspace/board identity.
|
|
908
|
+
await purgeBoardMachineLocalCaches({ projectRoot: request.projectRoot, localRoot });
|
|
758
909
|
if (resumedSession) {
|
|
759
910
|
const appliedAt = new Date().toISOString();
|
|
760
911
|
await updateGuidedSession({
|
|
@@ -980,6 +1131,27 @@ async function run(request) {
|
|
|
980
1131
|
!option(request, 'dryRun', false) &&
|
|
981
1132
|
!option(request, 'reviewOnly', false) &&
|
|
982
1133
|
nativeAdvisors;
|
|
1134
|
+
// Preflight the structured-provider key on a non-offline, non-native preview so
|
|
1135
|
+
// `run --preview` names a missing key before any cycle starts, rather than
|
|
1136
|
+
// surfacing it only when a real cycle reaches the provider path. Native and
|
|
1137
|
+
// offline runs never need the structured key, so they skip the check.
|
|
1138
|
+
const previewProviderWarnings = [];
|
|
1139
|
+
if (option(request, 'preview', false) &&
|
|
1140
|
+
!option(request, 'offline', false) &&
|
|
1141
|
+
!option(request, 'reviewOnly', false) &&
|
|
1142
|
+
!nativeAdvisors) {
|
|
1143
|
+
const openPlanrConfig = await loadConfig(request.projectRoot).catch(() => null);
|
|
1144
|
+
const readiness = openPlanrConfig
|
|
1145
|
+
? await resolveAIProviderReadiness(openPlanrConfig)
|
|
1146
|
+
: {
|
|
1147
|
+
configured: false,
|
|
1148
|
+
keyResolvable: false,
|
|
1149
|
+
remedy: 'No AI provider is configured. Run `planr config set-provider <name>` then `planr config set-key <provider>`, or run offline with --offline.',
|
|
1150
|
+
};
|
|
1151
|
+
if (!readiness.keyResolvable && readiness.remedy) {
|
|
1152
|
+
previewProviderWarnings.push(readiness.remedy);
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
983
1155
|
const result = await runOperatingCycle({
|
|
984
1156
|
projectRoot: request.projectRoot,
|
|
985
1157
|
cycleId: option(request, 'cycleId', undefined),
|
|
@@ -1041,7 +1213,13 @@ async function run(request) {
|
|
|
1041
1213
|
},
|
|
1042
1214
|
counts,
|
|
1043
1215
|
handoff,
|
|
1044
|
-
warnings: [
|
|
1216
|
+
warnings: [
|
|
1217
|
+
...new Set([
|
|
1218
|
+
...(result.cycle.warnings ?? []),
|
|
1219
|
+
...stringList(projected?.warnings),
|
|
1220
|
+
...previewProviderWarnings,
|
|
1221
|
+
]),
|
|
1222
|
+
],
|
|
1045
1223
|
nextActions,
|
|
1046
1224
|
data: result,
|
|
1047
1225
|
next: nextActions,
|
|
@@ -1049,10 +1227,16 @@ async function run(request) {
|
|
|
1049
1227
|
}
|
|
1050
1228
|
async function reviewOrBrief(request) {
|
|
1051
1229
|
const cycleId = argument(request, 'cycleId');
|
|
1230
|
+
// FR3/E-003: the human review gate renders report Markdown (brief + per-role
|
|
1231
|
+
// lens reports + exact next actions), never a raw `JSON.stringify` of the
|
|
1232
|
+
// state. `--json` keeps returning the exact raw state object, byte-unchanged.
|
|
1233
|
+
const human = request.action === 'review' && !option(request, 'json', false);
|
|
1052
1234
|
const data = await readOperatingReview({
|
|
1053
1235
|
projectRoot: request.projectRoot,
|
|
1054
1236
|
cycleId,
|
|
1055
1237
|
brief: request.action === 'brief',
|
|
1238
|
+
human,
|
|
1239
|
+
localRoot: option(request, 'localRoot', undefined),
|
|
1056
1240
|
});
|
|
1057
1241
|
return success(request.action, {
|
|
1058
1242
|
data,
|
|
@@ -1469,6 +1653,18 @@ export async function executeOperateAction(request) {
|
|
|
1469
1653
|
return await attachStructuredActions(request, await handler(request));
|
|
1470
1654
|
}
|
|
1471
1655
|
catch (error) {
|
|
1656
|
+
// FR7/E-007: first-use provider consent is a healthy continuation, not a
|
|
1657
|
+
// failure — return the `ok: true` handoff shape instead of an exit-4 error.
|
|
1658
|
+
// Every other authority/error stays a genuine failure.
|
|
1659
|
+
if (isProviderConsentHandoff(error)) {
|
|
1660
|
+
const continuation = providerConsentContinuation(request.action, error);
|
|
1661
|
+
try {
|
|
1662
|
+
return await attachStructuredActions(request, continuation);
|
|
1663
|
+
}
|
|
1664
|
+
catch {
|
|
1665
|
+
return continuation;
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1472
1668
|
try {
|
|
1473
1669
|
return await attachStructuredActions(request, failure(request.action, error));
|
|
1474
1670
|
}
|