openplanr 1.21.2 → 1.22.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/doctor.d.ts.map +1 -1
- package/dist/cli/commands/doctor.js +11 -1
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/operate.d.ts.map +1 -1
- package/dist/cli/commands/operate.js +91 -2
- package/dist/cli/commands/operate.js.map +1 -1
- package/dist/services/operate/advisors.d.ts +63 -0
- package/dist/services/operate/advisors.d.ts.map +1 -1
- package/dist/services/operate/advisors.js +68 -4
- package/dist/services/operate/advisors.js.map +1 -1
- package/dist/services/operate/artifacts.d.ts +9 -3
- package/dist/services/operate/artifacts.d.ts.map +1 -1
- package/dist/services/operate/artifacts.js +126 -5
- package/dist/services/operate/artifacts.js.map +1 -1
- package/dist/services/operate/citation-resolution.d.ts +14 -0
- package/dist/services/operate/citation-resolution.d.ts.map +1 -1
- package/dist/services/operate/citation-resolution.js +9 -0
- package/dist/services/operate/citation-resolution.js.map +1 -1
- package/dist/services/operate/completion.d.ts +80 -0
- package/dist/services/operate/completion.d.ts.map +1 -0
- package/dist/services/operate/completion.js +279 -0
- package/dist/services/operate/completion.js.map +1 -0
- package/dist/services/operate/config.d.ts +7 -0
- package/dist/services/operate/config.d.ts.map +1 -1
- package/dist/services/operate/config.js +7 -0
- package/dist/services/operate/config.js.map +1 -1
- package/dist/services/operate/context-research.d.ts +43 -0
- package/dist/services/operate/context-research.d.ts.map +1 -1
- package/dist/services/operate/context-research.js +219 -1
- package/dist/services/operate/context-research.js.map +1 -1
- package/dist/services/operate/decision-brief.d.ts +90 -1
- package/dist/services/operate/decision-brief.d.ts.map +1 -1
- package/dist/services/operate/decision-brief.js +175 -1
- package/dist/services/operate/decision-brief.js.map +1 -1
- package/dist/services/operate/doctor.d.ts +58 -0
- package/dist/services/operate/doctor.d.ts.map +1 -1
- package/dist/services/operate/doctor.js +395 -21
- package/dist/services/operate/doctor.js.map +1 -1
- package/dist/services/operate/engine.d.ts +75 -2
- package/dist/services/operate/engine.d.ts.map +1 -1
- package/dist/services/operate/engine.js +504 -33
- package/dist/services/operate/engine.js.map +1 -1
- package/dist/services/operate/event-store.d.ts +22 -0
- package/dist/services/operate/event-store.d.ts.map +1 -1
- package/dist/services/operate/event-store.js +35 -0
- package/dist/services/operate/event-store.js.map +1 -1
- package/dist/services/operate/evidence-cache.d.ts.map +1 -1
- package/dist/services/operate/evidence-cache.js +41 -5
- package/dist/services/operate/evidence-cache.js.map +1 -1
- package/dist/services/operate/index.d.ts.map +1 -1
- package/dist/services/operate/index.js +98 -1
- package/dist/services/operate/index.js.map +1 -1
- package/dist/services/operate/interaction/question-engine.d.ts.map +1 -1
- package/dist/services/operate/interaction/question-engine.js +36 -8
- package/dist/services/operate/interaction/question-engine.js.map +1 -1
- package/dist/services/operate/interaction/question-registry.d.ts +8 -0
- package/dist/services/operate/interaction/question-registry.d.ts.map +1 -1
- package/dist/services/operate/interaction/question-registry.js +21 -2
- package/dist/services/operate/interaction/question-registry.js.map +1 -1
- package/dist/services/operate/lifecycle-driver.d.ts +309 -0
- package/dist/services/operate/lifecycle-driver.d.ts.map +1 -0
- package/dist/services/operate/lifecycle-driver.js +434 -0
- package/dist/services/operate/lifecycle-driver.js.map +1 -0
- package/dist/services/operate/maintenance.d.ts +65 -1
- package/dist/services/operate/maintenance.d.ts.map +1 -1
- package/dist/services/operate/maintenance.js +718 -24
- package/dist/services/operate/maintenance.js.map +1 -1
- package/dist/services/operate/mission-dispatch.d.ts +61 -1
- package/dist/services/operate/mission-dispatch.d.ts.map +1 -1
- package/dist/services/operate/mission-dispatch.js +78 -5
- package/dist/services/operate/mission-dispatch.js.map +1 -1
- package/dist/services/operate/profile-migration.d.ts +69 -0
- package/dist/services/operate/profile-migration.d.ts.map +1 -0
- package/dist/services/operate/profile-migration.js +462 -0
- package/dist/services/operate/profile-migration.js.map +1 -0
- package/dist/services/operate/projection-persistence.d.ts.map +1 -1
- package/dist/services/operate/projection-persistence.js +24 -5
- package/dist/services/operate/projection-persistence.js.map +1 -1
- package/dist/services/operate/projection.d.ts.map +1 -1
- package/dist/services/operate/projection.js +9 -0
- package/dist/services/operate/projection.js.map +1 -1
- package/dist/services/operate/protocol.d.ts +1 -0
- package/dist/services/operate/protocol.d.ts.map +1 -1
- package/dist/services/operate/protocol.js.map +1 -1
- package/dist/services/operate/scratch.d.ts +81 -0
- package/dist/services/operate/scratch.d.ts.map +1 -0
- package/dist/services/operate/scratch.js +190 -0
- package/dist/services/operate/scratch.js.map +1 -0
- package/dist/services/operate/types.d.ts +3 -2
- package/dist/services/operate/types.d.ts.map +1 -1
- package/dist/services/operate/types.js.map +1 -1
- package/dist/services/operate/workspace.d.ts +1 -0
- package/dist/services/operate/workspace.d.ts.map +1 -1
- package/dist/services/operate/workspace.js +2 -0
- package/dist/services/operate/workspace.js.map +1 -1
- package/dist/services/runtime-manager-service.d.ts +23 -0
- package/dist/services/runtime-manager-service.d.ts.map +1 -1
- package/dist/services/runtime-manager-service.js +30 -0
- package/dist/services/runtime-manager-service.js.map +1 -1
- package/package.json +2 -2
|
@@ -5,6 +5,34 @@ export interface OperatingDoctorDiagnostic {
|
|
|
5
5
|
message: string;
|
|
6
6
|
fix?: string;
|
|
7
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* FR9 (T-008): the AGENT-CONTRACT decision, isolated as a pure function so a
|
|
10
|
+
* genuine role/provider/boundary divergence in the installed contract can be
|
|
11
|
+
* exercised directly. This owns the ONLY fail path for the installed contract;
|
|
12
|
+
* a board persisted at an older protocol envelope is explicitly not its concern
|
|
13
|
+
* (that is `diagnoseBoardStateVersion`). The fail message names which facet —
|
|
14
|
+
* roles, providers, or read-only boundaries — actually diverged.
|
|
15
|
+
*/
|
|
16
|
+
export declare function evaluateAgentContract(input: {
|
|
17
|
+
roleIds: string[];
|
|
18
|
+
providerIds: string[];
|
|
19
|
+
boundariesValid: boolean;
|
|
20
|
+
pipelineVersion?: string;
|
|
21
|
+
}): OperatingDoctorDiagnostic;
|
|
22
|
+
/**
|
|
23
|
+
* FR9 (T-008): report the persisted board-state protocol version as a SEPARATE
|
|
24
|
+
* fact from the installed agent contract. A board written at an older protocol
|
|
25
|
+
* envelope than the current agent contract stays fully readable — the frozen
|
|
26
|
+
* additive-envelope guarantee — so an older board is reported informationally
|
|
27
|
+
* (`pass`), NEVER `warn`/`fail` "incompatible". The genuine-mismatch failure
|
|
28
|
+
* path belongs exclusively to `evaluateAgentContract` and the boundary/id
|
|
29
|
+
* checks. A board written at a NEWER contract than is installed is the only
|
|
30
|
+
* version drift that warns (this install may not implement its newer surface),
|
|
31
|
+
* and even then the message never frames the older/newer relationship itself as
|
|
32
|
+
* an incompatibility. Pure over `(boardStateVersion, agentContractVersion)` so
|
|
33
|
+
* the mapping is testable without a board on disk.
|
|
34
|
+
*/
|
|
35
|
+
export declare function diagnoseBoardStateVersion(boardStateVersion: string | null, agentContractVersion?: string): OperatingDoctorDiagnostic;
|
|
8
36
|
/**
|
|
9
37
|
* FR7: cycle integrity is a first-class readable-tree surface. This is the
|
|
10
38
|
* regression guard on that surface — a citation rejection or boundary refusal
|
|
@@ -18,6 +46,36 @@ export interface OperatingDoctorDiagnostic {
|
|
|
18
46
|
* without a full board on disk.
|
|
19
47
|
*/
|
|
20
48
|
export declare function diagnoseOperatingCycleIntegrity(state: OperatingState, cycleId: string | null, integrityFileContent: string | null): OperatingDoctorDiagnostic;
|
|
49
|
+
/**
|
|
50
|
+
* FR6 (T-005): the internal transport contract is exactly that — internal. The
|
|
51
|
+
* user sees validated Markdown, concise progress, and a final synthesis, never a
|
|
52
|
+
* lease token, an idempotency key, an evidence digest, or a harness/adapter
|
|
53
|
+
* lifecycle command; JSON is available only under an explicit `--json`. These
|
|
54
|
+
* patterns are the forbidden field/command tokens as they appear when raw
|
|
55
|
+
* transport leaks into a human surface. They deliberately do NOT match the plain
|
|
56
|
+
* English words ("release"/"please" have no word boundary before "lease"; a
|
|
57
|
+
* finding that merely mentions "the runtime adapter" carries no lifecycle verb),
|
|
58
|
+
* so the check flags a genuine leak rather than ordinary advisory prose.
|
|
59
|
+
*/
|
|
60
|
+
export declare const OPERATING_TRANSPORT_LEAK_PATTERNS: ReadonlyArray<{
|
|
61
|
+
field: string;
|
|
62
|
+
pattern: RegExp;
|
|
63
|
+
}>;
|
|
64
|
+
/**
|
|
65
|
+
* Return the internal-transport field/command tokens that leaked into one block
|
|
66
|
+
* of human-facing text. Empty when the text is clean. Shared with the CLI
|
|
67
|
+
* regression test so both guards agree on exactly what counts as a leak.
|
|
68
|
+
*/
|
|
69
|
+
export declare function scanOperatingTransportLeakage(text: string): string[];
|
|
70
|
+
/**
|
|
71
|
+
* FR6: fail when any of the forbidden transport tokens appears in the non-`--json`
|
|
72
|
+
* output of `status`/`report`/`review`. Pure over the assembled surfaces so a
|
|
73
|
+
* deliberate leak can be exercised directly (and a clean board proven clean).
|
|
74
|
+
*/
|
|
75
|
+
export declare function evaluateOperatingTransportLeakage(surfaces: ReadonlyArray<{
|
|
76
|
+
surface: string;
|
|
77
|
+
text: string;
|
|
78
|
+
}>): OperatingDoctorDiagnostic;
|
|
21
79
|
export declare function diagnoseOperatingBoard(input: {
|
|
22
80
|
projectRoot: string;
|
|
23
81
|
localRoot?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../../src/services/operate/doctor.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../../src/services/operate/doctor.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAIjD,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AA6BD;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE;IAC3C,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,eAAe,EAAE,OAAO,CAAC;IACzB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B,GAAG,yBAAyB,CAoB5B;AAgED;;;;;;;;;;;;GAYG;AACH,wBAAgB,yBAAyB,CACvC,iBAAiB,EAAE,MAAM,GAAG,IAAI,EAChC,oBAAoB,GAAE,MAAuC,GAC5D,yBAAyB,CA6B3B;AA0jBD;;;;;;;;;;;GAWG;AACH,wBAAgB,+BAA+B,CAC7C,KAAK,EAAE,cAAc,EACrB,OAAO,EAAE,MAAM,GAAG,IAAI,EACtB,oBAAoB,EAAE,MAAM,GAAG,IAAI,GAClC,yBAAyB,CAmC3B;AAqJD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,iCAAiC,EAAE,aAAa,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAc7F,CAAC;AAEJ;;;;GAIG;AACH,wBAAgB,6BAA6B,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAMpE;AAED;;;;GAIG;AACH,wBAAgB,iCAAiC,CAC/C,QAAQ,EAAE,aAAa,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,GACzD,yBAAyB,CAuB3B;AA2FD,wBAAsB,sBAAsB,CAAC,KAAK,EAAE;IAClD,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,GAAG,OAAO,CAAC,yBAAyB,EAAE,CAAC,CAyEvC"}
|
|
@@ -2,6 +2,7 @@ import { existsSync } from 'node:fs';
|
|
|
2
2
|
import { readdir, readFile } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { canonicalDigest } from './canonical.js';
|
|
5
|
+
import { verifyOperatingCompletionPhases } from './completion.js';
|
|
5
6
|
import { operatingProjectKey } from './config.js';
|
|
6
7
|
import { OperatingEventStore } from './event-store.js';
|
|
7
8
|
import { buildOperatingIntegritySummary, detectGitignoredWorkspace } from './integrity.js';
|
|
@@ -10,8 +11,12 @@ import { readJournal } from './journal.js';
|
|
|
10
11
|
import { readOperatingLock } from './lock-service.js';
|
|
11
12
|
import { detectOperatingStorageLayout } from './migration.js';
|
|
12
13
|
import { classifyOperatingRuntime, resolveActiveOperatingRuntime } from './mission-dispatch.js';
|
|
14
|
+
import { compareLegacyProfileToOperatingConfig } from './profile-migration.js';
|
|
15
|
+
import { renderOperatingBrief } from './projection.js';
|
|
13
16
|
import { inspectOperatingProjectionDrift } from './projection-persistence.js';
|
|
14
17
|
import { loadOperatingProtocol, operatingPipelineAvailable } from './protocol.js';
|
|
18
|
+
import { listAbandonedOperatingScratch } from './scratch.js';
|
|
19
|
+
import { OPERATE_AGENT_PROTOCOL_VERSION } from './types.js';
|
|
15
20
|
import { resolveOperatingPaths } from './workspace.js';
|
|
16
21
|
const EXPECTED_ROLES = [
|
|
17
22
|
'strategy-finance',
|
|
@@ -34,7 +39,44 @@ function sameIds(actual, expected) {
|
|
|
34
39
|
return (actual.length === expected.length &&
|
|
35
40
|
[...actual].sort().join('\0') === [...expected].sort().join('\0'));
|
|
36
41
|
}
|
|
37
|
-
|
|
42
|
+
/**
|
|
43
|
+
* FR9 (T-008): the AGENT-CONTRACT decision, isolated as a pure function so a
|
|
44
|
+
* genuine role/provider/boundary divergence in the installed contract can be
|
|
45
|
+
* exercised directly. This owns the ONLY fail path for the installed contract;
|
|
46
|
+
* a board persisted at an older protocol envelope is explicitly not its concern
|
|
47
|
+
* (that is `diagnoseBoardStateVersion`). The fail message names which facet —
|
|
48
|
+
* roles, providers, or read-only boundaries — actually diverged.
|
|
49
|
+
*/
|
|
50
|
+
export function evaluateAgentContract(input) {
|
|
51
|
+
const diverged = [];
|
|
52
|
+
if (!sameIds(input.roleIds, EXPECTED_ROLES))
|
|
53
|
+
diverged.push('roles');
|
|
54
|
+
if (!sameIds(input.providerIds, EXPECTED_PROVIDERS))
|
|
55
|
+
diverged.push('providers');
|
|
56
|
+
if (!input.boundariesValid)
|
|
57
|
+
diverged.push('read-only boundaries');
|
|
58
|
+
if (diverged.length > 0) {
|
|
59
|
+
return {
|
|
60
|
+
code: 'operate-protocol',
|
|
61
|
+
status: 'fail',
|
|
62
|
+
message: `Operating Board agent contract (Protocol v${OPERATE_AGENT_PROTOCOL_VERSION}) does not match the certified role/provider contract: ${diverged.join(', ')} diverge`,
|
|
63
|
+
fix: 'Install the exact OpenPlanr release dependencies, then rerun `planr doctor`.',
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
code: 'operate-protocol',
|
|
68
|
+
status: 'pass',
|
|
69
|
+
message: `Operating Board agent contract (Protocol v${OPERATE_AGENT_PROTOCOL_VERSION}) registries are compatible${input.pipelineVersion ? ` with planr-pipeline ${input.pipelineVersion}` : ''}`,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* FR9 (T-008): report the currently installed agent contract (registry/role/
|
|
74
|
+
* provider check) as ONE fact. Split out of the former conflated
|
|
75
|
+
* `diagnoseProtocol`; the persisted board-state protocol version is reported
|
|
76
|
+
* separately by `diagnoseBoardStateVersion` so an older persisted board never
|
|
77
|
+
* reads as an incompatibility here.
|
|
78
|
+
*/
|
|
79
|
+
async function diagnoseAgentContractVersion(pipelineVersion) {
|
|
38
80
|
if (!operatingPipelineAvailable()) {
|
|
39
81
|
return {
|
|
40
82
|
code: 'operate-protocol',
|
|
@@ -47,35 +89,95 @@ async function diagnoseProtocol(pipelineVersion) {
|
|
|
47
89
|
const protocol = await loadOperatingProtocol();
|
|
48
90
|
const roles = protocol.listOperatingRoles();
|
|
49
91
|
const providers = protocol.listOperatingProviders();
|
|
50
|
-
const roleIds = roles.map((entry) => entry.id);
|
|
51
|
-
const providerIds = providers.map((entry) => entry.id);
|
|
52
92
|
const boundariesValid = roles.every((entry) => entry.readOnly === true &&
|
|
53
93
|
['none', 'governed-output-only'].includes(String(entry.writeBoundary))) && providers.every((entry) => entry.readOnly === true);
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
message: 'Operating Board Protocol v1.4 registries do not match the certified role/provider contract',
|
|
61
|
-
fix: 'Install the exact OpenPlanr release dependencies, then rerun `planr doctor`.',
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
return {
|
|
65
|
-
code: 'operate-protocol',
|
|
66
|
-
status: 'pass',
|
|
67
|
-
message: `Operating Board Protocol v1.4 registries are compatible${pipelineVersion ? ` with planr-pipeline ${pipelineVersion}` : ''}`,
|
|
68
|
-
};
|
|
94
|
+
return evaluateAgentContract({
|
|
95
|
+
roleIds: roles.map((entry) => entry.id),
|
|
96
|
+
providerIds: providers.map((entry) => entry.id),
|
|
97
|
+
boundariesValid,
|
|
98
|
+
pipelineVersion,
|
|
99
|
+
});
|
|
69
100
|
}
|
|
70
101
|
catch (error) {
|
|
71
102
|
return {
|
|
72
103
|
code: 'operate-protocol',
|
|
73
104
|
status: 'fail',
|
|
74
|
-
message: `Operating Board Protocol
|
|
105
|
+
message: `Operating Board agent contract (Protocol v${OPERATE_AGENT_PROTOCOL_VERSION}) compatibility check failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
75
106
|
fix: 'Install the exact OpenPlanr release dependencies, then rerun `planr doctor`.',
|
|
76
107
|
};
|
|
77
108
|
}
|
|
78
109
|
}
|
|
110
|
+
function compareProtocolVersions(left, right) {
|
|
111
|
+
const parse = (value) => value.split('.').map((part) => {
|
|
112
|
+
const parsed = Number.parseInt(part, 10);
|
|
113
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
114
|
+
});
|
|
115
|
+
const a = parse(left);
|
|
116
|
+
const b = parse(right);
|
|
117
|
+
for (let index = 0; index < 3; index += 1) {
|
|
118
|
+
const da = a[index] ?? 0;
|
|
119
|
+
const db = b[index] ?? 0;
|
|
120
|
+
if (da !== db)
|
|
121
|
+
return da < db ? -1 : 1;
|
|
122
|
+
}
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* FR9 (T-008): report the persisted board-state protocol version as a SEPARATE
|
|
127
|
+
* fact from the installed agent contract. A board written at an older protocol
|
|
128
|
+
* envelope than the current agent contract stays fully readable — the frozen
|
|
129
|
+
* additive-envelope guarantee — so an older board is reported informationally
|
|
130
|
+
* (`pass`), NEVER `warn`/`fail` "incompatible". The genuine-mismatch failure
|
|
131
|
+
* path belongs exclusively to `evaluateAgentContract` and the boundary/id
|
|
132
|
+
* checks. A board written at a NEWER contract than is installed is the only
|
|
133
|
+
* version drift that warns (this install may not implement its newer surface),
|
|
134
|
+
* and even then the message never frames the older/newer relationship itself as
|
|
135
|
+
* an incompatibility. Pure over `(boardStateVersion, agentContractVersion)` so
|
|
136
|
+
* the mapping is testable without a board on disk.
|
|
137
|
+
*/
|
|
138
|
+
export function diagnoseBoardStateVersion(boardStateVersion, agentContractVersion = OPERATE_AGENT_PROTOCOL_VERSION) {
|
|
139
|
+
if (!boardStateVersion) {
|
|
140
|
+
return {
|
|
141
|
+
code: 'operate-board-state-version',
|
|
142
|
+
status: 'pass',
|
|
143
|
+
message: 'No persisted Operating Board state is present yet to report a protocol version for',
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
const ordering = compareProtocolVersions(boardStateVersion, agentContractVersion);
|
|
147
|
+
if (ordering > 0) {
|
|
148
|
+
return {
|
|
149
|
+
code: 'operate-board-state-version',
|
|
150
|
+
status: 'warn',
|
|
151
|
+
message: `Persisted Operating Board state is at Protocol v${boardStateVersion}, newer than the installed agent contract (Protocol v${agentContractVersion}); this installation may not implement its newer surface`,
|
|
152
|
+
fix: 'Run `npm install -g openplanr@latest`, then `planr setup --scope user`, to match the agent contract to the persisted board state.',
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
if (ordering < 0) {
|
|
156
|
+
return {
|
|
157
|
+
code: 'operate-board-state-version',
|
|
158
|
+
status: 'pass',
|
|
159
|
+
message: `Persisted Operating Board state is at Protocol v${boardStateVersion}, readable under the current agent contract (Protocol v${agentContractVersion}); an older persisted board is expected and is not an incompatibility`,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
code: 'operate-board-state-version',
|
|
164
|
+
status: 'pass',
|
|
165
|
+
message: `Persisted Operating Board state is at Protocol v${boardStateVersion}, matching the installed agent contract (Protocol v${agentContractVersion})`,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
async function diagnoseBoardStateVersionSurface(projectRoot, localRoot) {
|
|
169
|
+
let boardStateVersion = null;
|
|
170
|
+
try {
|
|
171
|
+
const state = await new OperatingEventStore(projectRoot, { localRoot }).state();
|
|
172
|
+
boardStateVersion = typeof state.protocolVersion === 'string' ? state.protocolVersion : null;
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
// A broken/absent event chain is reported by the event-replay check; the
|
|
176
|
+
// board-state protocol version is only meaningful against a readable board.
|
|
177
|
+
boardStateVersion = null;
|
|
178
|
+
}
|
|
179
|
+
return diagnoseBoardStateVersion(boardStateVersion, OPERATE_AGENT_PROTOCOL_VERSION);
|
|
180
|
+
}
|
|
79
181
|
async function diagnoseEventState(projectRoot, localRoot) {
|
|
80
182
|
const diagnostics = [];
|
|
81
183
|
const store = new OperatingEventStore(projectRoot, { localRoot });
|
|
@@ -523,6 +625,37 @@ async function diagnoseIncrementalBaselines(projectRoot, localRoot) {
|
|
|
523
625
|
message: 'Incremental evidence baselines match the committed workspace digest',
|
|
524
626
|
};
|
|
525
627
|
}
|
|
628
|
+
/**
|
|
629
|
+
* FR7 (T-006): detect OpenPlanr-owned scratch left behind by a session that never
|
|
630
|
+
* finalized. The reproduction lost completed analyses because the runtime chose
|
|
631
|
+
* its own temp-file transport with nothing owning it; scratch now lives under the
|
|
632
|
+
* project-and-machine-keyed operate root, is recorded in an owned manifest, and is
|
|
633
|
+
* cleaned automatically after a successful record/finalize. Any scratch still
|
|
634
|
+
* carrying an owned manifest past its lease window is abandoned. This reads only
|
|
635
|
+
* machine-local scratch, names abandoned sets by their cycle key (never a raw
|
|
636
|
+
* project or home path), and points at the FR7-named `planr doctor --fix`, which
|
|
637
|
+
* delegates to the single owned-only cleanup that removes ONLY confirmed
|
|
638
|
+
* OpenPlanr-owned stale scratch — never an arbitrary file found under the
|
|
639
|
+
* directory.
|
|
640
|
+
*/
|
|
641
|
+
async function diagnoseAbandonedScratch(projectRoot, localRoot) {
|
|
642
|
+
const paths = resolveOperatingPaths(projectRoot, { localRoot });
|
|
643
|
+
const abandoned = await listAbandonedOperatingScratch(paths);
|
|
644
|
+
if (abandoned.length > 0) {
|
|
645
|
+
const cycles = abandoned.map((entry) => entry.cycleId).join(', ');
|
|
646
|
+
return {
|
|
647
|
+
code: 'operate-scratch',
|
|
648
|
+
status: 'warn',
|
|
649
|
+
message: `${abandoned.length} OpenPlanr-owned scratch set(s) were left behind by a session that never finalized: ${cycles}`,
|
|
650
|
+
fix: 'Run `planr doctor --fix` to remove only the confirmed OpenPlanr-owned stale scratch.',
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
return {
|
|
654
|
+
code: 'operate-scratch',
|
|
655
|
+
status: 'pass',
|
|
656
|
+
message: 'No abandoned OpenPlanr-owned operate scratch is present',
|
|
657
|
+
};
|
|
658
|
+
}
|
|
526
659
|
/**
|
|
527
660
|
* Classify the active runtime for agent-native Operate. Enforced isolation and
|
|
528
661
|
* runtime-governed native workflows are both first-class; only a missing or
|
|
@@ -642,8 +775,232 @@ async function diagnoseWorkspaceVersioning(projectRoot) {
|
|
|
642
775
|
: {}),
|
|
643
776
|
};
|
|
644
777
|
}
|
|
778
|
+
/**
|
|
779
|
+
* FR10 / T-009: report drift between a legacy `.planr/operate-profile.json` and
|
|
780
|
+
* the live operating configuration. A stale profile that still names fields the
|
|
781
|
+
* current config no longer carries (its `id`, role selection, caps, providers, or
|
|
782
|
+
* budgets) is exactly what made guided init suggest a profile the CLI then
|
|
783
|
+
* rejected; naming the differing fields points the operator at
|
|
784
|
+
* `planr operate profiles migrate inspect`. Reads only; a missing profile or a
|
|
785
|
+
* missing/unreadable config is a clean pass because there is nothing to reconcile.
|
|
786
|
+
*/
|
|
787
|
+
async function diagnoseProfileDrift(projectRoot, localRoot) {
|
|
788
|
+
const profileRaw = await readFile(path.join(projectRoot, '.planr', 'operate-profile.json'), 'utf8').catch((error) => {
|
|
789
|
+
if (error.code === 'ENOENT')
|
|
790
|
+
return null;
|
|
791
|
+
throw error;
|
|
792
|
+
});
|
|
793
|
+
if (profileRaw === null) {
|
|
794
|
+
return {
|
|
795
|
+
code: 'operate-profile-drift',
|
|
796
|
+
status: 'pass',
|
|
797
|
+
message: 'No legacy .planr/operate-profile.json is present to reconcile',
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
let profile;
|
|
801
|
+
try {
|
|
802
|
+
profile = JSON.parse(profileRaw);
|
|
803
|
+
}
|
|
804
|
+
catch {
|
|
805
|
+
return {
|
|
806
|
+
code: 'operate-profile-drift',
|
|
807
|
+
status: 'warn',
|
|
808
|
+
message: 'Legacy .planr/operate-profile.json is not valid JSON and cannot be reconciled with the operating configuration',
|
|
809
|
+
fix: 'Run `planr operate profiles migrate inspect` to review the legacy profile before migrating it.',
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
if (!profile || typeof profile !== 'object' || Array.isArray(profile)) {
|
|
813
|
+
return {
|
|
814
|
+
code: 'operate-profile-drift',
|
|
815
|
+
status: 'warn',
|
|
816
|
+
message: 'Legacy .planr/operate-profile.json is not a JSON object and cannot be reconciled',
|
|
817
|
+
fix: 'Run `planr operate profiles migrate inspect` to review the legacy profile before migrating it.',
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
const configRaw = await readFile(resolveOperatingPaths(projectRoot, { localRoot }).config, 'utf8').catch((error) => {
|
|
821
|
+
if (error.code === 'ENOENT')
|
|
822
|
+
return null;
|
|
823
|
+
throw error;
|
|
824
|
+
});
|
|
825
|
+
if (configRaw === null) {
|
|
826
|
+
return {
|
|
827
|
+
code: 'operate-profile-drift',
|
|
828
|
+
status: 'pass',
|
|
829
|
+
message: 'No operating configuration is present to compare the legacy profile against',
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
let config;
|
|
833
|
+
try {
|
|
834
|
+
config = JSON.parse(configRaw);
|
|
835
|
+
}
|
|
836
|
+
catch {
|
|
837
|
+
return {
|
|
838
|
+
code: 'operate-profile-drift',
|
|
839
|
+
status: 'pass',
|
|
840
|
+
message: 'Operating configuration is unreadable; the legacy profile cannot be compared to it',
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
const drift = compareLegacyProfileToOperatingConfig(profile, (config ?? {}));
|
|
844
|
+
if (drift.length > 0) {
|
|
845
|
+
return {
|
|
846
|
+
code: 'operate-profile-drift',
|
|
847
|
+
status: 'warn',
|
|
848
|
+
message: `Legacy .planr/operate-profile.json drifts from the operating configuration in ${drift.length} field(s): ${drift.join(', ')}`,
|
|
849
|
+
fix: 'Run `planr operate profiles migrate inspect`, then `planr operate profiles migrate apply --yes` to reconcile the legacy profile.',
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
return {
|
|
853
|
+
code: 'operate-profile-drift',
|
|
854
|
+
status: 'pass',
|
|
855
|
+
message: 'Legacy .planr/operate-profile.json matches the operating configuration',
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* FR6 (T-005): the internal transport contract is exactly that — internal. The
|
|
860
|
+
* user sees validated Markdown, concise progress, and a final synthesis, never a
|
|
861
|
+
* lease token, an idempotency key, an evidence digest, or a harness/adapter
|
|
862
|
+
* lifecycle command; JSON is available only under an explicit `--json`. These
|
|
863
|
+
* patterns are the forbidden field/command tokens as they appear when raw
|
|
864
|
+
* transport leaks into a human surface. They deliberately do NOT match the plain
|
|
865
|
+
* English words ("release"/"please" have no word boundary before "lease"; a
|
|
866
|
+
* finding that merely mentions "the runtime adapter" carries no lifecycle verb),
|
|
867
|
+
* so the check flags a genuine leak rather than ordinary advisory prose.
|
|
868
|
+
*/
|
|
869
|
+
export const OPERATING_TRANSPORT_LEAK_PATTERNS = [
|
|
870
|
+
{
|
|
871
|
+
field: 'lease',
|
|
872
|
+
pattern: /\blease\s*(?:token|id|status|expires?|expiry|remaining|duration|nonce)\b|\blease(?:Token|Id|Status|Expires\w*|Remaining\w*|Duration\w*|Nonce)\b|"lease"\s*:/i,
|
|
873
|
+
},
|
|
874
|
+
{ field: 'idempotencyKey', pattern: /\bidempotency[\s._-]?key\b|\bidempotencyKey\b/i },
|
|
875
|
+
{ field: 'evidenceDigest', pattern: /\bevidence[\s._-]?digest\b|\bevidenceDigest\b/i },
|
|
876
|
+
{
|
|
877
|
+
field: 'harness/adapter command',
|
|
878
|
+
pattern: /\b(?:harness|adapter)(?:\.[a-z]+|\s+(?:prepare|record|finalize|resume|cancel|heartbeat))\b/i,
|
|
879
|
+
},
|
|
880
|
+
];
|
|
881
|
+
/**
|
|
882
|
+
* Return the internal-transport field/command tokens that leaked into one block
|
|
883
|
+
* of human-facing text. Empty when the text is clean. Shared with the CLI
|
|
884
|
+
* regression test so both guards agree on exactly what counts as a leak.
|
|
885
|
+
*/
|
|
886
|
+
export function scanOperatingTransportLeakage(text) {
|
|
887
|
+
const found = new Set();
|
|
888
|
+
for (const { field, pattern } of OPERATING_TRANSPORT_LEAK_PATTERNS) {
|
|
889
|
+
if (pattern.test(text))
|
|
890
|
+
found.add(field);
|
|
891
|
+
}
|
|
892
|
+
return [...found];
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* FR6: fail when any of the forbidden transport tokens appears in the non-`--json`
|
|
896
|
+
* output of `status`/`report`/`review`. Pure over the assembled surfaces so a
|
|
897
|
+
* deliberate leak can be exercised directly (and a clean board proven clean).
|
|
898
|
+
*/
|
|
899
|
+
export function evaluateOperatingTransportLeakage(surfaces) {
|
|
900
|
+
const leaks = [];
|
|
901
|
+
for (const { surface, text } of surfaces) {
|
|
902
|
+
for (const field of scanOperatingTransportLeakage(text)) {
|
|
903
|
+
leaks.push(`${surface}: ${field}`);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
if (leaks.length > 0) {
|
|
907
|
+
return {
|
|
908
|
+
code: 'operate-transport-hiding',
|
|
909
|
+
status: 'fail',
|
|
910
|
+
message: `Internal transport leaked into non-JSON operate output — ${[...new Set(leaks)]
|
|
911
|
+
.sort()
|
|
912
|
+
.join(', ')}`,
|
|
913
|
+
fix: 'Keep lease tokens, idempotency keys, evidence digests, and harness/adapter lifecycle commands inside the `--json` surface only.',
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
return {
|
|
917
|
+
code: 'operate-transport-hiding',
|
|
918
|
+
status: 'pass',
|
|
919
|
+
message: 'No internal transport (lease, idempotency key, evidence digest, or harness/adapter command) appears in status/report/review human output',
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
async function diagnoseTransportHiding(projectRoot, localRoot) {
|
|
923
|
+
const surfaces = [];
|
|
924
|
+
try {
|
|
925
|
+
const state = await new OperatingEventStore(projectRoot, { localRoot }).state();
|
|
926
|
+
// `status` renders the concise brief as its human summary.
|
|
927
|
+
surfaces.push({ surface: 'status', text: renderOperatingBrief(state) });
|
|
928
|
+
}
|
|
929
|
+
catch {
|
|
930
|
+
// No readable state yet; the event-replay check owns a broken chain.
|
|
931
|
+
}
|
|
932
|
+
try {
|
|
933
|
+
const { readOperatingReport } = await import('./reports.js');
|
|
934
|
+
const report = await readOperatingReport({ projectRoot, localRoot });
|
|
935
|
+
// `report` renders this Markdown directly; `review`'s human gate reuses the
|
|
936
|
+
// same assembly, so scanning it once covers both surfaces.
|
|
937
|
+
surfaces.push({ surface: 'report/review', text: report.markdown });
|
|
938
|
+
}
|
|
939
|
+
catch {
|
|
940
|
+
// No governed cycle to report yet.
|
|
941
|
+
}
|
|
942
|
+
if (surfaces.length === 0) {
|
|
943
|
+
return {
|
|
944
|
+
code: 'operate-transport-hiding',
|
|
945
|
+
status: 'pass',
|
|
946
|
+
message: 'No operate human output is available to scan for transport leakage yet',
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
return evaluateOperatingTransportLeakage(surfaces);
|
|
950
|
+
}
|
|
951
|
+
/**
|
|
952
|
+
* FR14 (T-005): a cycle is `reviewable` in committed state ONLY if its phase-F
|
|
953
|
+
* review-gate artifacts genuinely exist on disk. A projection that claims
|
|
954
|
+
* `reviewable` without the recorded roles, Chair result, final report, actions
|
|
955
|
+
* file, and draft/provenance results is exactly the dishonest completion the spec
|
|
956
|
+
* forbids, and it fails here with the actual phase each cycle reached.
|
|
957
|
+
*/
|
|
958
|
+
async function diagnoseCompletionDiscipline(projectRoot, localRoot) {
|
|
959
|
+
let state;
|
|
960
|
+
try {
|
|
961
|
+
state = await new OperatingEventStore(projectRoot, { localRoot }).state();
|
|
962
|
+
}
|
|
963
|
+
catch {
|
|
964
|
+
return {
|
|
965
|
+
code: 'operate-completion',
|
|
966
|
+
status: 'pass',
|
|
967
|
+
message: 'No readable board state is available to verify completion discipline yet',
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
const paths = resolveOperatingPaths(projectRoot, { localRoot });
|
|
971
|
+
const reviewable = state.cycles.filter((cycle) => cycle.state === 'reviewable');
|
|
972
|
+
if (reviewable.length === 0) {
|
|
973
|
+
return {
|
|
974
|
+
code: 'operate-completion',
|
|
975
|
+
status: 'pass',
|
|
976
|
+
message: 'No cycle is marked reviewable; nothing to verify against the phase-F review gate',
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
const incomplete = [];
|
|
980
|
+
for (const cycle of reviewable) {
|
|
981
|
+
const result = await verifyOperatingCompletionPhases(state, cycle.id, paths);
|
|
982
|
+
if (!result.complete)
|
|
983
|
+
incomplete.push({ cycleId: cycle.id, result });
|
|
984
|
+
}
|
|
985
|
+
if (incomplete.length > 0) {
|
|
986
|
+
const detail = incomplete
|
|
987
|
+
.map(({ cycleId, result }) => `${cycleId} (reached phase ${result.reachedPhase ?? 'none'}; missing ${result.missing.join('; ') || 'phase-F artifacts'})`)
|
|
988
|
+
.join(', ');
|
|
989
|
+
return {
|
|
990
|
+
code: 'operate-completion',
|
|
991
|
+
status: 'fail',
|
|
992
|
+
message: `${incomplete.length} cycle(s) are marked reviewable without their phase-F review-gate artifacts on disk: ${detail}`,
|
|
993
|
+
fix: 'Run `planr operate cycles recover <cycleId> --yes` to re-materialize the review-gate artifacts from the verified event chain.',
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
return {
|
|
997
|
+
code: 'operate-completion',
|
|
998
|
+
status: 'pass',
|
|
999
|
+
message: `${reviewable.length} reviewable cycle(s) carry every phase-F review-gate artifact on disk`,
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
645
1002
|
export async function diagnoseOperatingBoard(input) {
|
|
646
|
-
const diagnostics = [await
|
|
1003
|
+
const diagnostics = [await diagnoseAgentContractVersion(input.pipelineVersion)];
|
|
647
1004
|
const paths = resolveOperatingPaths(input.projectRoot, { localRoot: input.localRoot });
|
|
648
1005
|
if (!existsSync(paths.root))
|
|
649
1006
|
return diagnostics;
|
|
@@ -685,12 +1042,29 @@ export async function diagnoseOperatingBoard(input) {
|
|
|
685
1042
|
// FR11: the two staleness detectors for the machine-local caches FR4 binds
|
|
686
1043
|
// to board identity — stale adapter sessions and stale incremental baselines.
|
|
687
1044
|
await diagnoseAdapterSessions(input.projectRoot, input.localRoot), await diagnoseIncrementalBaselines(input.projectRoot, input.localRoot),
|
|
1045
|
+
// FR7 (T-006): detect OpenPlanr-owned scratch abandoned by a session that
|
|
1046
|
+
// never finalized; the scoped purge removes only confirmed owned stale scratch.
|
|
1047
|
+
await diagnoseAbandonedScratch(input.projectRoot, input.localRoot),
|
|
688
1048
|
// FR7: cycle integrity is a first-class readable surface — this guards it
|
|
689
1049
|
// against regression and reports its three conditions explicitly.
|
|
690
1050
|
await diagnoseCycleIntegritySurface(input.projectRoot, input.localRoot),
|
|
1051
|
+
// FR9 (T-008): report the persisted board-state protocol version as its own
|
|
1052
|
+
// fact, separate from the agent-contract check above, so an older persisted
|
|
1053
|
+
// board is never rendered as an incompatibility.
|
|
1054
|
+
await diagnoseBoardStateVersionSurface(input.projectRoot, input.localRoot),
|
|
691
1055
|
// FR9: state plainly whether a gitignored `.planr/` leaves the board
|
|
692
1056
|
// unversioned, rather than implying it is tracked.
|
|
693
|
-
await diagnoseWorkspaceVersioning(input.projectRoot)
|
|
1057
|
+
await diagnoseWorkspaceVersioning(input.projectRoot),
|
|
1058
|
+
// FR10 (T-009): report drift between a legacy operate-profile.json and the
|
|
1059
|
+
// live operating configuration, naming the differing fields.
|
|
1060
|
+
await diagnoseProfileDrift(input.projectRoot, input.localRoot),
|
|
1061
|
+
// FR6 (T-005): fail if internal transport (lease, idempotency key, evidence
|
|
1062
|
+
// digest, or a harness/adapter command) leaked into status/report/review
|
|
1063
|
+
// human output — those belong to the `--json` surface only.
|
|
1064
|
+
await diagnoseTransportHiding(input.projectRoot, input.localRoot),
|
|
1065
|
+
// FR14 (T-005): fail if a cycle is marked reviewable in committed state
|
|
1066
|
+
// without its phase-F review-gate artifacts genuinely present on disk.
|
|
1067
|
+
await diagnoseCompletionDiscipline(input.projectRoot, input.localRoot));
|
|
694
1068
|
return diagnostics;
|
|
695
1069
|
}
|
|
696
1070
|
//# sourceMappingURL=doctor.js.map
|