openplanr 1.16.2 → 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 +240 -13
- 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 +82 -1
- package/dist/services/operate/advisors.d.ts.map +1 -1
- package/dist/services/operate/advisors.js +469 -17
- package/dist/services/operate/advisors.js.map +1 -1
- package/dist/services/operate/artifacts.d.ts +27 -1
- package/dist/services/operate/artifacts.d.ts.map +1 -1
- package/dist/services/operate/artifacts.js +103 -1
- package/dist/services/operate/artifacts.js.map +1 -1
- package/dist/services/operate/cadence.d.ts +50 -0
- package/dist/services/operate/cadence.d.ts.map +1 -0
- package/dist/services/operate/cadence.js +56 -0
- package/dist/services/operate/cadence.js.map +1 -0
- package/dist/services/operate/citation-resolution.d.ts +101 -0
- package/dist/services/operate/citation-resolution.d.ts.map +1 -0
- package/dist/services/operate/citation-resolution.js +308 -0
- package/dist/services/operate/citation-resolution.js.map +1 -0
- package/dist/services/operate/config.d.ts +93 -1
- package/dist/services/operate/config.d.ts.map +1 -1
- package/dist/services/operate/config.js +199 -9
- package/dist/services/operate/config.js.map +1 -1
- package/dist/services/operate/decision-brief.d.ts +148 -0
- package/dist/services/operate/decision-brief.d.ts.map +1 -0
- package/dist/services/operate/decision-brief.js +194 -0
- package/dist/services/operate/decision-brief.js.map +1 -0
- package/dist/services/operate/doctor.d.ts.map +1 -1
- package/dist/services/operate/doctor.js +271 -3
- package/dist/services/operate/doctor.js.map +1 -1
- package/dist/services/operate/engine.d.ts +66 -2
- package/dist/services/operate/engine.d.ts.map +1 -1
- package/dist/services/operate/engine.js +219 -7
- package/dist/services/operate/engine.js.map +1 -1
- package/dist/services/operate/event-store.d.ts +35 -1
- package/dist/services/operate/event-store.d.ts.map +1 -1
- package/dist/services/operate/event-store.js +142 -29
- package/dist/services/operate/event-store.js.map +1 -1
- package/dist/services/operate/evidence-cache.d.ts +25 -1
- package/dist/services/operate/evidence-cache.d.ts.map +1 -1
- package/dist/services/operate/evidence-cache.js +62 -1
- package/dist/services/operate/evidence-cache.js.map +1 -1
- package/dist/services/operate/evidence.d.ts +20 -1
- package/dist/services/operate/evidence.d.ts.map +1 -1
- package/dist/services/operate/evidence.js +148 -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 +309 -46
- package/dist/services/operate/index.js.map +1 -1
- package/dist/services/operate/interaction/action-service.d.ts +17 -0
- package/dist/services/operate/interaction/action-service.d.ts.map +1 -1
- package/dist/services/operate/interaction/action-service.js +19 -0
- package/dist/services/operate/interaction/action-service.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/journal.d.ts.map +1 -1
- package/dist/services/operate/journal.js +11 -5
- package/dist/services/operate/journal.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 +406 -46
- package/dist/services/operate/maintenance.js.map +1 -1
- package/dist/services/operate/migration.d.ts +68 -0
- package/dist/services/operate/migration.d.ts.map +1 -0
- package/dist/services/operate/migration.js +235 -0
- package/dist/services/operate/migration.js.map +1 -0
- package/dist/services/operate/mission-dispatch.d.ts +193 -0
- package/dist/services/operate/mission-dispatch.d.ts.map +1 -0
- package/dist/services/operate/mission-dispatch.js +444 -0
- package/dist/services/operate/mission-dispatch.js.map +1 -0
- package/dist/services/operate/projection-persistence.d.ts +6 -0
- package/dist/services/operate/projection-persistence.d.ts.map +1 -1
- package/dist/services/operate/projection-persistence.js +87 -11
- package/dist/services/operate/projection-persistence.js.map +1 -1
- package/dist/services/operate/projection.d.ts +26 -1
- package/dist/services/operate/projection.d.ts.map +1 -1
- package/dist/services/operate/projection.js +62 -0
- package/dist/services/operate/projection.js.map +1 -1
- package/dist/services/operate/protocol.d.ts +50 -3
- package/dist/services/operate/protocol.d.ts.map +1 -1
- package/dist/services/operate/protocol.js +57 -5
- package/dist/services/operate/protocol.js.map +1 -1
- package/dist/services/operate/read-only-providers.d.ts +37 -0
- package/dist/services/operate/read-only-providers.d.ts.map +1 -1
- package/dist/services/operate/read-only-providers.js +130 -0
- package/dist/services/operate/read-only-providers.js.map +1 -1
- package/dist/services/operate/reports.d.ts +22 -0
- package/dist/services/operate/reports.d.ts.map +1 -1
- package/dist/services/operate/reports.js +226 -1
- package/dist/services/operate/reports.js.map +1 -1
- package/dist/services/operate/routes.d.ts.map +1 -1
- package/dist/services/operate/routes.js +79 -5
- package/dist/services/operate/routes.js.map +1 -1
- package/dist/services/operate/types.d.ts +139 -4
- package/dist/services/operate/types.d.ts.map +1 -1
- package/dist/services/operate/types.js +13 -0
- package/dist/services/operate/types.js.map +1 -1
- package/dist/services/operate/workspace.d.ts +7 -0
- package/dist/services/operate/workspace.d.ts.map +1 -1
- package/dist/services/operate/workspace.js +14 -5
- package/dist/services/operate/workspace.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,14 +1,17 @@
|
|
|
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';
|
|
5
|
+
import { computeNextDueAt } from './cadence.js';
|
|
3
6
|
import { canonicalDigest, sha256Digest } from './canonical.js';
|
|
4
|
-
import { applyOperatingInitialization, getOperatingProfile, listOperatingProfiles, normalizeCustomOperatingProfile, normalizeOperatingInitializationAnswers, prepareOperatingInitialization, validateOperatingConfiguration, } from './config.js';
|
|
7
|
+
import { applyOperatingInitialization, getOperatingProfile, listOperatingProfiles, normalizeCustomOperatingProfile, normalizeOperatingInitializationAnswers, parseOperatingDispatchModeOverrideFlags, prepareOperatingInitialization, readOperatingLastRunAt, validateOperatingConfiguration, } from './config.js';
|
|
5
8
|
import { runOperatingCycle } from './engine.js';
|
|
6
9
|
import { OperatingEventStore } from './event-store.js';
|
|
7
10
|
import { classifyEvidenceDiagnostic } from './evidence-classifications.js';
|
|
8
11
|
import { listEvidenceDiagnostics, readEvidenceDiagnostic } from './evidence-diagnostics.js';
|
|
9
12
|
import { parseStrictJson, readImportedEvidenceFile } from './evidence-import.js';
|
|
10
13
|
import { createOperatingAction } from './interaction/action-service.js';
|
|
11
|
-
import { persistableOperatingInitAnswers, resumeGuidedSession, submitGuidedAnswers, } from './interaction/answer-service.js';
|
|
14
|
+
import { persistableOperatingInitAnswers, probeAvailableEvidenceSources, probeGitUserName, probePipelineInstalled, resumeGuidedSession, submitGuidedAnswers, } from './interaction/answer-service.js';
|
|
12
15
|
import { assertOperatingConfirmation } from './interaction/confirmation-service.js';
|
|
13
16
|
import { decodeOperatingInitializationReplay, encodeOperatingInitializationReplay, } from './interaction/initialization-replay.js';
|
|
14
17
|
import { createOperatingInitQuestionnaire, evaluateOperatingInitQuestions, operatingInitAnswersFromOptions, } from './interaction/question-engine.js';
|
|
@@ -16,7 +19,8 @@ import { cancelGuidedSession, createGuidedSession, createGuidedSessionId, curren
|
|
|
16
19
|
import { assertCommittedOperatingView } from './journal.js';
|
|
17
20
|
import { applyOperatingMigration, inspectOperatingMigration, rollbackOperatingMigration, } from './legacy-import-service.js';
|
|
18
21
|
import { answerOperatingGap, applyOrRollbackRoute, decideOperatingDecision, governOperatingFinding, readOperatingCollection, readOperatingReview, transitionOperatingCycle, verifyOperatingGap, } from './lifecycle.js';
|
|
19
|
-
import { createOperatingAdapterStartHandoff, exportOperatingDiagnostics, operateAdapterLifecycle, operatingCacheAction, operatingIntegrityAction, repairOperatingSecurity, } from './maintenance.js';
|
|
22
|
+
import { createOperatingAdapterStartHandoff, exportOperatingDiagnostics, operateAdapterLifecycle, operatingCacheAction, operatingIntegrityAction, purgeBoardMachineLocalCaches, repairOperatingSecurity, } from './maintenance.js';
|
|
23
|
+
import { migrateOperatingStorageLayoutOnOpen } from './migration.js';
|
|
20
24
|
import { renderOperatingBrief } from './projection.js';
|
|
21
25
|
import { loadOperatingProtocol, resolveOperatingPipelineRoot } from './protocol.js';
|
|
22
26
|
import { executeGitHubReadOnly, executeGitReadOnly } from './read-only-providers.js';
|
|
@@ -48,17 +52,43 @@ function stringList(value) {
|
|
|
48
52
|
.filter(Boolean);
|
|
49
53
|
return [];
|
|
50
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
|
+
}
|
|
51
74
|
async function resolvedOperatingRuntime(projectRoot, requested) {
|
|
52
75
|
if (requested !== 'auto')
|
|
53
76
|
return requested;
|
|
54
|
-
|
|
77
|
+
const persisted = await readFile(path.join(resolveOperatingPaths(projectRoot).localRoot, 'preferences.json'), 'utf8')
|
|
55
78
|
.then((raw) => {
|
|
56
79
|
const runtime = JSON.parse(raw).runtime;
|
|
57
80
|
return typeof runtime === 'string' && runtime ? runtime : 'auto';
|
|
58
81
|
})
|
|
59
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';
|
|
60
90
|
}
|
|
61
|
-
async function usesNativeOperatingAdvisors(projectRoot, requestedRuntime) {
|
|
91
|
+
export async function usesNativeOperatingAdvisors(projectRoot, requestedRuntime) {
|
|
62
92
|
const runtime = await resolvedOperatingRuntime(projectRoot, requestedRuntime);
|
|
63
93
|
const adapterId = runtime === 'claude' ? 'claude-code' : runtime;
|
|
64
94
|
if (adapterId === 'auto')
|
|
@@ -72,7 +102,10 @@ async function usesNativeOperatingAdvisors(projectRoot, requestedRuntime) {
|
|
|
72
102
|
const adapter = registry.adapters?.find((entry) => entry.id === adapterId);
|
|
73
103
|
if (adapter?.capabilities?.operatingBoard !== true)
|
|
74
104
|
return false;
|
|
75
|
-
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 ?? '')) {
|
|
76
109
|
return true;
|
|
77
110
|
}
|
|
78
111
|
// Compatibility with Protocol v1.2 registries published before the
|
|
@@ -121,6 +154,47 @@ function success(action, value = {}) {
|
|
|
121
154
|
nextActions,
|
|
122
155
|
};
|
|
123
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
|
+
}
|
|
124
198
|
/**
|
|
125
199
|
* Stable process exit classes for automation.
|
|
126
200
|
*
|
|
@@ -133,6 +207,15 @@ function success(action, value = {}) {
|
|
|
133
207
|
*
|
|
134
208
|
* Keep this exhaustive so adding a public Operate error cannot silently fall
|
|
135
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.
|
|
136
219
|
*/
|
|
137
220
|
const OPERATE_EXIT_CODES = {
|
|
138
221
|
E_OPERATE_INTERNAL: 1,
|
|
@@ -178,13 +261,23 @@ const OPERATE_EXIT_CODES = {
|
|
|
178
261
|
E_OPERATE_EVIDENCE_REJECTED: 6,
|
|
179
262
|
E_OPERATE_PROVIDER_READ_ONLY: 6,
|
|
180
263
|
E_OPERATE_SECRET_DETECTED: 6,
|
|
264
|
+
E_OPERATE_MISSION_PACKET_BUDGET: 6,
|
|
265
|
+
E_OPERATE_MISSION_UNAVAILABLE: 3,
|
|
181
266
|
E_OPERATE_SESSION_INVALID: 2,
|
|
182
267
|
};
|
|
183
268
|
function operateExitCode(code) {
|
|
184
269
|
return OPERATE_EXIT_CODES[code];
|
|
185
270
|
}
|
|
186
|
-
function failure(action, error) {
|
|
271
|
+
export function failure(action, error) {
|
|
187
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;
|
|
188
281
|
const confirmationAction = error instanceof OperateError &&
|
|
189
282
|
error.details?.action &&
|
|
190
283
|
typeof error.details.action === 'object' &&
|
|
@@ -244,11 +337,29 @@ function failure(action, error) {
|
|
|
244
337
|
counts: {},
|
|
245
338
|
warnings: [],
|
|
246
339
|
nextActions,
|
|
247
|
-
data: error instanceof OperateError
|
|
340
|
+
data: error instanceof OperateError
|
|
341
|
+
? error.details
|
|
342
|
+
: internalErrorClass
|
|
343
|
+
? { errorClass: internalErrorClass }
|
|
344
|
+
: undefined,
|
|
248
345
|
next: nextActions,
|
|
249
346
|
exitCode: operateExitCode(code),
|
|
250
347
|
};
|
|
251
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
|
+
*/
|
|
252
363
|
function publicActionCommands(nextActions) {
|
|
253
364
|
return [
|
|
254
365
|
...new Set(nextActions
|
|
@@ -261,6 +372,17 @@ function publicActionCommands(nextActions) {
|
|
|
261
372
|
.filter((value) => /^planr\s+/.test(value))),
|
|
262
373
|
];
|
|
263
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
|
+
}
|
|
264
386
|
function hasFlag(command, flag) {
|
|
265
387
|
return new RegExp(`(?:^|\\s)${flag.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?:\\s|$)`).test(command);
|
|
266
388
|
}
|
|
@@ -296,8 +418,9 @@ async function actionEffect(request, command) {
|
|
|
296
418
|
return 'project-write';
|
|
297
419
|
}
|
|
298
420
|
async function attachStructuredActions(request, result) {
|
|
299
|
-
if (result.actions?.length || result.action === 'input_required')
|
|
421
|
+
if (result.actions?.length || result.action === 'input_required' || result.flow === 'handoff') {
|
|
300
422
|
return result;
|
|
423
|
+
}
|
|
301
424
|
const commands = publicActionCommands(result.nextActions);
|
|
302
425
|
if (commands.length === 0)
|
|
303
426
|
return result;
|
|
@@ -323,6 +446,7 @@ async function attachStructuredActions(request, result) {
|
|
|
323
446
|
const actions = [];
|
|
324
447
|
for (const [index, command] of commands.entries()) {
|
|
325
448
|
const effect = await actionEffect(request, command);
|
|
449
|
+
const confirmFlag = commandConfirmFlag(command);
|
|
326
450
|
const id = `operate.next.${canonicalDigest({ command }).slice('sha256:'.length, 'sha256:'.length + 20)}`.toLowerCase();
|
|
327
451
|
const created = await createOperatingAction({
|
|
328
452
|
id,
|
|
@@ -352,8 +476,29 @@ async function attachStructuredActions(request, result) {
|
|
|
352
476
|
},
|
|
353
477
|
}),
|
|
354
478
|
});
|
|
355
|
-
|
|
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);
|
|
356
499
|
}
|
|
500
|
+
if (actions.length === 0)
|
|
501
|
+
return result;
|
|
357
502
|
return { ...result, actions };
|
|
358
503
|
}
|
|
359
504
|
async function inspect(request) {
|
|
@@ -559,22 +704,14 @@ async function initialize(request) {
|
|
|
559
704
|
});
|
|
560
705
|
supplied = normalizeOperatingInitializationAnswers(resumedSession.answers);
|
|
561
706
|
if (resumedSession.status === 'input-required') {
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
action: 'input_required',
|
|
567
|
-
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', {
|
|
568
711
|
message: 'Operating Board initialization needs explicit human input.',
|
|
569
712
|
state: resumedSession.session.state,
|
|
570
|
-
paths: {},
|
|
571
|
-
counts: {},
|
|
572
|
-
warnings: [],
|
|
573
|
-
nextActions: [],
|
|
574
|
-
next: [],
|
|
575
713
|
questionnaire: resumedSession.questionnaire,
|
|
576
|
-
|
|
577
|
-
};
|
|
714
|
+
});
|
|
578
715
|
}
|
|
579
716
|
}
|
|
580
717
|
let customProfile;
|
|
@@ -586,13 +723,36 @@ async function initialize(request) {
|
|
|
586
723
|
supplied.profileFile) {
|
|
587
724
|
throw new OperateError('E_OPERATE_CONFIG_INVALID', '--profile-file is valid only with --profile custom.');
|
|
588
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
|
+
]);
|
|
589
740
|
const context = {
|
|
590
741
|
projectRoot: request.projectRoot,
|
|
591
742
|
...bindings,
|
|
743
|
+
...(detectedHostRuntime ? { detectedRuntime: detectedHostRuntime } : {}),
|
|
744
|
+
...(gitUserName ? { gitUserName } : {}),
|
|
592
745
|
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
593
|
-
availableSources
|
|
594
|
-
|
|
595
|
-
|
|
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',
|
|
596
756
|
};
|
|
597
757
|
const questionState = await evaluateOperatingInitQuestions({
|
|
598
758
|
answers: supplied,
|
|
@@ -613,28 +773,24 @@ async function initialize(request) {
|
|
|
613
773
|
persistedAnswers: persistableOperatingInitAnswers(questionState.answers, context),
|
|
614
774
|
localRoot,
|
|
615
775
|
});
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
action: 'input_required',
|
|
621
|
-
code: 'E_OPERATE_INPUT_REQUIRED',
|
|
622
|
-
message: 'Operating Board initialization needs explicit human input.',
|
|
623
|
-
state: null,
|
|
624
|
-
paths: {},
|
|
625
|
-
counts: {},
|
|
626
|
-
warnings: [],
|
|
627
|
-
nextActions: [],
|
|
628
|
-
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', {
|
|
629
780
|
questionnaire,
|
|
630
|
-
|
|
631
|
-
};
|
|
781
|
+
});
|
|
632
782
|
}
|
|
633
783
|
const profile = supplied.profile;
|
|
634
784
|
const decisionOwner = supplied.decisionOwner ?? option(request, 'decisionOwner', '').trim();
|
|
635
785
|
const planningEngine = supplied.planningEngine ??
|
|
636
786
|
option(request, 'planningEngine', 'openplanr');
|
|
637
787
|
const rawCharter = supplied.charter ?? option(request, 'charter', {});
|
|
788
|
+
// FR4 / E-004 fold-in: forward the CLI-validated per-project dispatch-mode
|
|
789
|
+
// overrides into initialization so they are part of the committed machine-local
|
|
790
|
+
// preferences (and thus the preview digest), rather than a separate post-apply
|
|
791
|
+
// patch. Omitted entirely when no override flag was supplied, keeping the
|
|
792
|
+
// no-override preview digest byte-identical.
|
|
793
|
+
const dispatchModeOverrides = parseOperatingDispatchModeOverrideFlags(stringList(request.options.dispatchModeOverride));
|
|
638
794
|
const preview = await prepareOperatingInitialization({
|
|
639
795
|
projectRoot: request.projectRoot,
|
|
640
796
|
profile,
|
|
@@ -642,6 +798,7 @@ async function initialize(request) {
|
|
|
642
798
|
planningEngine,
|
|
643
799
|
runtime: supplied.runtime ?? option(request, 'runtime', 'auto'),
|
|
644
800
|
cadence: supplied.cadence ?? option(request, 'cadence', 'manual'),
|
|
801
|
+
...(Object.keys(dispatchModeOverrides).length > 0 ? { dispatchModeOverrides } : {}),
|
|
645
802
|
timezone: supplied.timezone ??
|
|
646
803
|
option(request, 'timezone', Intl.DateTimeFormat().resolvedOptions().timeZone),
|
|
647
804
|
sensitivityCeiling: supplied.sensitivityCeiling ?? option(request, 'sensitivityCeiling', 'internal'),
|
|
@@ -744,6 +901,11 @@ async function initialize(request) {
|
|
|
744
901
|
preview,
|
|
745
902
|
confirmationDigest: preview.previewDigest,
|
|
746
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 });
|
|
747
909
|
if (resumedSession) {
|
|
748
910
|
const appliedAt = new Date().toISOString();
|
|
749
911
|
await updateGuidedSession({
|
|
@@ -935,16 +1097,24 @@ async function testOperatingSource(request, provider) {
|
|
|
935
1097
|
return { provider, healthy: true, observation, writeBoundary: 'none' };
|
|
936
1098
|
}
|
|
937
1099
|
async function status(request) {
|
|
938
|
-
await validateOperatingConfiguration(request.projectRoot);
|
|
939
|
-
|
|
940
|
-
|
|
1100
|
+
const config = await validateOperatingConfiguration(request.projectRoot);
|
|
1101
|
+
const localRoot = option(request, 'localRoot', undefined);
|
|
1102
|
+
await assertCommittedOperatingView(request.projectRoot, localRoot ? { localRoot } : {});
|
|
1103
|
+
const store = new OperatingEventStore(request.projectRoot, localRoot ? { localRoot } : {});
|
|
941
1104
|
const state = await store.state();
|
|
942
1105
|
const activeCycle = [...state.cycles]
|
|
943
1106
|
.filter((cycle) => !['closed', 'cancelled'].includes(cycle.state))
|
|
944
1107
|
.sort((left, right) => left.id.localeCompare(right.id))
|
|
945
1108
|
.at(-1);
|
|
1109
|
+
// FR8 / E-008: surface `nextDueAt` via the pipeline's pure calculator with an
|
|
1110
|
+
// INJECTED clock. This CLI boundary supplies `now` (an explicit option or the
|
|
1111
|
+
// wall clock); the calculator itself reads no `Date.now()`. `manual` → null;
|
|
1112
|
+
// `weekly` / `monthly` → the computed due date from the persisted `lastRunAt`.
|
|
1113
|
+
const now = option(request, 'now', undefined) ?? new Date().toISOString();
|
|
1114
|
+
const lastRunAt = await readOperatingLastRunAt(request.projectRoot, localRoot ? { localRoot } : {});
|
|
1115
|
+
const nextDueAt = await computeNextDueAt(config.cadence, lastRunAt, now);
|
|
946
1116
|
return success(request.action, {
|
|
947
|
-
data: state,
|
|
1117
|
+
data: { ...state, cadence: { mode: config.cadence, lastRunAt, nextDueAt } },
|
|
948
1118
|
message: activeCycle?.state === 'blocked'
|
|
949
1119
|
? `Operating Board is blocked on ${state.summary.openGaps} evidence or advisor readiness gap(s).`
|
|
950
1120
|
: state.summary.quiet
|
|
@@ -961,6 +1131,27 @@ async function run(request) {
|
|
|
961
1131
|
!option(request, 'dryRun', false) &&
|
|
962
1132
|
!option(request, 'reviewOnly', false) &&
|
|
963
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
|
+
}
|
|
964
1155
|
const result = await runOperatingCycle({
|
|
965
1156
|
projectRoot: request.projectRoot,
|
|
966
1157
|
cycleId: option(request, 'cycleId', undefined),
|
|
@@ -1022,7 +1213,13 @@ async function run(request) {
|
|
|
1022
1213
|
},
|
|
1023
1214
|
counts,
|
|
1024
1215
|
handoff,
|
|
1025
|
-
warnings: [
|
|
1216
|
+
warnings: [
|
|
1217
|
+
...new Set([
|
|
1218
|
+
...(result.cycle.warnings ?? []),
|
|
1219
|
+
...stringList(projected?.warnings),
|
|
1220
|
+
...previewProviderWarnings,
|
|
1221
|
+
]),
|
|
1222
|
+
],
|
|
1026
1223
|
nextActions,
|
|
1027
1224
|
data: result,
|
|
1028
1225
|
next: nextActions,
|
|
@@ -1030,10 +1227,16 @@ async function run(request) {
|
|
|
1030
1227
|
}
|
|
1031
1228
|
async function reviewOrBrief(request) {
|
|
1032
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);
|
|
1033
1234
|
const data = await readOperatingReview({
|
|
1034
1235
|
projectRoot: request.projectRoot,
|
|
1035
1236
|
cycleId,
|
|
1036
1237
|
brief: request.action === 'brief',
|
|
1238
|
+
human,
|
|
1239
|
+
localRoot: option(request, 'localRoot', undefined),
|
|
1037
1240
|
});
|
|
1038
1241
|
return success(request.action, {
|
|
1039
1242
|
data,
|
|
@@ -1388,6 +1591,47 @@ const HANDLERS = {
|
|
|
1388
1591
|
'adapter.finalize': maintenance,
|
|
1389
1592
|
'adapter.cancel': maintenance,
|
|
1390
1593
|
};
|
|
1594
|
+
/**
|
|
1595
|
+
* Actions that only read committed state. A SPEC-002-layout project stays
|
|
1596
|
+
* readable through these without being migrated; only a mutating action opening
|
|
1597
|
+
* such a project triggers the automatic, journal-safe v1.3 migration (FR5/E-005).
|
|
1598
|
+
*/
|
|
1599
|
+
const OPERATE_READ_ONLY_ACTIONS = new Set([
|
|
1600
|
+
'inspect',
|
|
1601
|
+
'demo',
|
|
1602
|
+
'config.show',
|
|
1603
|
+
'config.validate',
|
|
1604
|
+
'config.edit',
|
|
1605
|
+
'profiles.list',
|
|
1606
|
+
'profiles.show',
|
|
1607
|
+
'profiles.validate',
|
|
1608
|
+
'sources.list',
|
|
1609
|
+
'sources.show',
|
|
1610
|
+
'sources.test',
|
|
1611
|
+
'status',
|
|
1612
|
+
'review',
|
|
1613
|
+
'brief',
|
|
1614
|
+
'report',
|
|
1615
|
+
'cycles.list',
|
|
1616
|
+
'cycles.show',
|
|
1617
|
+
'findings.list',
|
|
1618
|
+
'findings.show',
|
|
1619
|
+
'routes.list',
|
|
1620
|
+
'routes.show',
|
|
1621
|
+
'decisions.list',
|
|
1622
|
+
'decisions.show',
|
|
1623
|
+
'gaps.list',
|
|
1624
|
+
'gaps.show',
|
|
1625
|
+
'evidence.list',
|
|
1626
|
+
'evidence.show',
|
|
1627
|
+
'evidence.diagnose',
|
|
1628
|
+
'migrate.inspect',
|
|
1629
|
+
'migrations.list',
|
|
1630
|
+
'migrations.show',
|
|
1631
|
+
'cache.status',
|
|
1632
|
+
'integrity.status',
|
|
1633
|
+
'adapter.resume',
|
|
1634
|
+
]);
|
|
1391
1635
|
/**
|
|
1392
1636
|
* Stable runtime-neutral Operating Board facade. Public CLI adapters only parse
|
|
1393
1637
|
* arguments and render this structured result; all state and security semantics
|
|
@@ -1399,9 +1643,28 @@ export async function executeOperateAction(request) {
|
|
|
1399
1643
|
if (!handler) {
|
|
1400
1644
|
throw new OperateError('E_OPERATE_ACTION_UNKNOWN', `Unknown Operating Board action: ${request.action}.`);
|
|
1401
1645
|
}
|
|
1646
|
+
// Any mutating action that opens a SPEC-002-layout project migrates it to the
|
|
1647
|
+
// v1.3 storage layout, through the write-ahead journal, before proceeding.
|
|
1648
|
+
if (!OPERATE_READ_ONLY_ACTIONS.has(request.action)) {
|
|
1649
|
+
await migrateOperatingStorageLayoutOnOpen(request.projectRoot, {
|
|
1650
|
+
localRoot: option(request, 'localRoot', undefined),
|
|
1651
|
+
});
|
|
1652
|
+
}
|
|
1402
1653
|
return await attachStructuredActions(request, await handler(request));
|
|
1403
1654
|
}
|
|
1404
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
|
+
}
|
|
1405
1668
|
try {
|
|
1406
1669
|
return await attachStructuredActions(request, failure(request.action, error));
|
|
1407
1670
|
}
|