fraim 2.0.350 → 2.0.351
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/registry/scripts/learning/run-application.js +61 -23
- package/dist/registry/scripts/learning/run-publication.js +40 -21
- package/dist/registry/scripts/learning/run-record.js +145 -36
- package/dist/src/cli/learning/queue-operations.js +3 -3
- package/dist/src/cli/mcp/fraim-mcp-latest-launcher.js +18 -1
- package/dist/src/cli/setup/user-level-sync.js +2 -0
- package/dist/src/local-mcp-server/learning-context-builder.js +16 -119
- package/package.json +1 -1
|
@@ -15,6 +15,7 @@ const run_record_1 = require("./run-record");
|
|
|
15
15
|
const owner_identity_1 = require("./owner-identity");
|
|
16
16
|
const proposal_queue_1 = require("./proposal-queue");
|
|
17
17
|
const entry_authoring_1 = require("./entry-authoring");
|
|
18
|
+
const run_publication_1 = require("./run-publication");
|
|
18
19
|
const completion_state_2 = require("./completion-state");
|
|
19
20
|
const artifact_paths_1 = require("../artifact-paths");
|
|
20
21
|
const learning_source_record_1 = require("../learning-usage/learning-source-record");
|
|
@@ -50,8 +51,11 @@ function capabilityGuards(action, proof, options) {
|
|
|
50
51
|
|| typeof snapshot.sha256 !== 'string' || !/^[a-f0-9]{64}$/.test(snapshot.sha256);
|
|
51
52
|
}))
|
|
52
53
|
throw new Error('capability-local-proof-required');
|
|
53
|
-
if (snapshots.get(node_path_1.default.resolve(required[0]))?.sha256 !== action.delivery?.expectedHash)
|
|
54
|
-
|
|
54
|
+
if (snapshots.get(node_path_1.default.resolve(required[0]))?.sha256 !== action.delivery?.expectedHash) {
|
|
55
|
+
if (typeof proof.reviewedContent !== 'string' || (0, proposal_queue_1.hashContent)(proof.reviewedContent) !== action.delivery?.expectedHash
|
|
56
|
+
|| !(0, run_publication_1.sameDeliveryText)(node_fs_1.default.readFileSync(required[0], 'utf8'), proof.reviewedContent))
|
|
57
|
+
throw new Error('capability-local-proof-conflict');
|
|
58
|
+
}
|
|
55
59
|
return [...snapshots.values()].map(item => {
|
|
56
60
|
const absent = item.realPath === null && item.sha256 === null;
|
|
57
61
|
if (!absent && (typeof item.realPath !== 'string' || typeof item.sha256 !== 'string' || !/^[a-f0-9]{64}$/.test(item.sha256)))
|
|
@@ -346,8 +350,11 @@ function applyRunAction(run, actionId, revision, expectedSequence, options) {
|
|
|
346
350
|
if (!delivered?.published || !delivered.reachable || delivered.contentHash !== action.delivery?.expectedHash)
|
|
347
351
|
throw new Error('capability-publication-or-reachability-pending');
|
|
348
352
|
withLocalGuards(capabilityGuards(action, delivered, options), () => {
|
|
349
|
-
if (action.application?.stage !== 'applied')
|
|
350
|
-
(0, proposal_queue_1.withFileLock)(run.file, () => run.progress(actionId, {
|
|
353
|
+
if (action.application?.stage !== 'applied' || (delivered.reviewedContent !== undefined && action.application.reviewedContent === undefined))
|
|
354
|
+
(0, proposal_queue_1.withFileLock)(run.file, () => run.progress(actionId, {
|
|
355
|
+
stage: 'applied', steps: [], publicationRef: delivered.reference,
|
|
356
|
+
...(delivered.reviewedContent !== undefined ? { reviewedContent: delivered.reviewedContent } : {}),
|
|
357
|
+
}));
|
|
351
358
|
});
|
|
352
359
|
return run.read().actions[actionId];
|
|
353
360
|
}
|
|
@@ -515,7 +522,8 @@ function sourceCompletion(run, homes, options, afterStage) {
|
|
|
515
522
|
const prior = state.processedSources;
|
|
516
523
|
const batch = (sources) => sources.map(source => ({ ...source, path: (0, completion_state_2.sourcePathKey)(source.path) })).sort((a, b) => a.path.localeCompare(b.path));
|
|
517
524
|
if (prior && (0, run_record_1.canonical)(batch(prior.map(({ path, sha256, kind }) => ({ path, sha256, kind })))) !== (0, run_record_1.canonical)(batch(normalized))
|
|
518
|
-
&& (!options.allowDeferred || prior.some(source => !normalized.some(item => (0, completion_state_2.sourcePathKey)(source.path) === (0, completion_state_2.sourcePathKey)(item.path) && source.sha256 === item.sha256 && source.kind === item.kind)
|
|
525
|
+
&& (!options.allowDeferred || prior.some(source => !normalized.some(item => (0, completion_state_2.sourcePathKey)(source.path) === (0, completion_state_2.sourcePathKey)(item.path) && source.sha256 === item.sha256 && source.kind === item.kind)
|
|
526
|
+
&& !options.deferredSources?.some(item => (0, completion_state_2.sourcePathKey)(source.path) === (0, completion_state_2.sourcePathKey)(item.path)))))
|
|
519
527
|
throw new Error('completion-sources-conflict');
|
|
520
528
|
const date = new Date().toISOString().slice(0, 10);
|
|
521
529
|
const validateInput = (body, source, root, contentRoot, owner, archivePath) => {
|
|
@@ -528,8 +536,11 @@ function sourceCompletion(run, homes, options, afterStage) {
|
|
|
528
536
|
};
|
|
529
537
|
const plans = normalized.map(source => {
|
|
530
538
|
const recorded = prior?.find(item => (0, completion_state_2.sourcePathKey)(item.path) === (0, completion_state_2.sourcePathKey)(source.path));
|
|
531
|
-
if (recorded)
|
|
539
|
+
if (recorded) {
|
|
540
|
+
if (source.sha256 !== recorded.sha256 || source.kind !== recorded.kind)
|
|
541
|
+
throw new Error(`completion-sources-conflict: ${source.path}: original plan changed`);
|
|
532
542
|
return recorded;
|
|
543
|
+
}
|
|
533
544
|
const { root, destination, contentRoot, owner, layout } = allowed(source);
|
|
534
545
|
regular(source.path, root);
|
|
535
546
|
regular(destination, root);
|
|
@@ -539,8 +550,9 @@ function sourceCompletion(run, homes, options, afterStage) {
|
|
|
539
550
|
validateInput(body, source, root, contentRoot, owner);
|
|
540
551
|
return { ...source, destination, resultHash: (0, proposal_queue_1.hashContent)(stamp(body, date)), synthesizedAt: date, layout };
|
|
541
552
|
});
|
|
542
|
-
|
|
543
|
-
|
|
553
|
+
const reservationsAndPlans = [...(prior ?? []), ...plans.filter(source => !prior?.some(item => (0, completion_state_2.sourcePathKey)(item.path) === (0, completion_state_2.sourcePathKey)(source.path)))];
|
|
554
|
+
if ((0, run_record_1.canonical)(prior) !== (0, run_record_1.canonical)(reservationsAndPlans)) {
|
|
555
|
+
run.prepareSourceCompletion(reservationsAndPlans, state.sequence, options.allowDeferred);
|
|
544
556
|
state = run.read();
|
|
545
557
|
}
|
|
546
558
|
afterStage?.('source-plan');
|
|
@@ -726,16 +738,21 @@ function completeRun(run, summary, homes, options, afterStage) {
|
|
|
726
738
|
sources.push({ path: ref, owner: home.owner, reason: 'action-processing-pending', ...(action.sourceHashes?.[ref] ? { sha256: action.sourceHashes[ref] } : {}) });
|
|
727
739
|
}
|
|
728
740
|
let reservationFailure;
|
|
741
|
+
let reserved = [];
|
|
729
742
|
try {
|
|
730
|
-
(0, completion_state_2.readSourceReservations)([...new Set([run.directory, ...options.runDirectories])], run.file);
|
|
743
|
+
reserved = (0, completion_state_2.readSourceReservations)([...new Set([run.directory, ...options.runDirectories])], run.file);
|
|
731
744
|
}
|
|
732
745
|
catch (error) {
|
|
733
746
|
reservationFailure = error.message;
|
|
734
747
|
}
|
|
735
|
-
|
|
748
|
+
const selected = [...(options.analyzedSources ?? [])];
|
|
749
|
+
for (const source of state.processedSources ?? [])
|
|
750
|
+
if (!selected.some(item => (0, completion_state_2.sourcePathKey)(item.path) === (0, completion_state_2.sourcePathKey)(source.path)))
|
|
751
|
+
selected.push({ path: source.path, sha256: source.sha256, kind: source.kind });
|
|
752
|
+
for (const source of selected) {
|
|
736
753
|
if (!source || !node_path_1.default.isAbsolute(source.path) || !/^[a-f0-9]{64}$/.test(source.sha256) || !['raw', 'retrospective'].includes(source.kind))
|
|
737
754
|
throw new Error('analyzed-sources-required');
|
|
738
|
-
if (state.
|
|
755
|
+
if (state.archivedSourcePaths?.includes((0, completion_state_2.sourcePathKey)(source.path)) || sources.some(item => (0, completion_state_2.sourcePathKey)(item.path) === (0, completion_state_2.sourcePathKey)(source.path)))
|
|
739
756
|
continue;
|
|
740
757
|
const home = sourceHome(source.path);
|
|
741
758
|
if (!home)
|
|
@@ -744,6 +761,19 @@ function completeRun(run, summary, homes, options, afterStage) {
|
|
|
744
761
|
if (!reason)
|
|
745
762
|
try {
|
|
746
763
|
(0, owner_identity_1.configuredArtifactPath)(home, source.path);
|
|
764
|
+
const recorded = state.processedSources?.find(item => (0, completion_state_2.sourcePathKey)(item.path) === (0, completion_state_2.sourcePathKey)(source.path));
|
|
765
|
+
if (recorded && (recorded.sha256 !== source.sha256 || recorded.kind !== source.kind))
|
|
766
|
+
throw new Error('original-source-plan-changed');
|
|
767
|
+
const locations = (0, artifact_paths_1.resolveArtifactPaths)((0, owner_identity_1.ownerArtifactRoot)(home), home.contentRoot);
|
|
768
|
+
const destination = (0, owner_identity_1.configuredArtifactPath)(home, node_path_1.default.join(source.kind === 'raw' ? locations.archiveRaw : locations.archiveRetrospectives, node_path_1.default.basename(source.path)));
|
|
769
|
+
if (reserved.includes((0, completion_state_2.sourcePathKey)(source.path)) || reserved.includes((0, completion_state_2.sourcePathKey)(destination)))
|
|
770
|
+
throw new Error('source-reserved-by-pending-run');
|
|
771
|
+
if (recorded && node_fs_1.default.existsSync(destination) && (0, proposal_queue_1.hashContent)(node_fs_1.default.readFileSync(destination, 'utf8')) !== recorded.resultHash)
|
|
772
|
+
throw new Error('source-archive-conflict');
|
|
773
|
+
// A verified copy with no pending original is an interrupted successful archive.
|
|
774
|
+
// The guarded finalizer revalidates its layout, identity and exact bytes.
|
|
775
|
+
if (recorded && !node_fs_1.default.existsSync(source.path) && node_fs_1.default.existsSync(destination))
|
|
776
|
+
continue;
|
|
747
777
|
const body = node_fs_1.default.readFileSync(source.path, 'utf8');
|
|
748
778
|
if ((0, proposal_queue_1.hashContent)(body) !== source.sha256)
|
|
749
779
|
throw new Error('analyzed-source-changed');
|
|
@@ -776,8 +806,11 @@ function completeRun(run, summary, homes, options, afterStage) {
|
|
|
776
806
|
const omitted = new Set(sources.map(source => (0, completion_state_2.sourcePathKey)(source.path)));
|
|
777
807
|
const inputs = [...(options.analyzedSources ?? [])].filter(source => !omitted.has((0, completion_state_2.sourcePathKey)(source.path)));
|
|
778
808
|
for (const source of state.processedSources ?? []) {
|
|
779
|
-
if (omitted.has((0, completion_state_2.sourcePathKey)(source.path)))
|
|
780
|
-
|
|
809
|
+
if (omitted.has((0, completion_state_2.sourcePathKey)(source.path))) {
|
|
810
|
+
if (state.archivedSourcePaths?.includes((0, completion_state_2.sourcePathKey)(source.path)))
|
|
811
|
+
throw new Error(`completed-source-cannot-be-deferred: ${source.path}`);
|
|
812
|
+
continue;
|
|
813
|
+
}
|
|
781
814
|
if (!inputs.some(input => (0, completion_state_2.sourcePathKey)(input.path) === (0, completion_state_2.sourcePathKey)(source.path)))
|
|
782
815
|
inputs.push({ path: source.path, sha256: source.sha256, kind: source.kind });
|
|
783
816
|
}
|
|
@@ -853,14 +886,16 @@ function completeRun(run, summary, homes, options, afterStage) {
|
|
|
853
886
|
for (const item of terminal.values()) {
|
|
854
887
|
const actual = item.step ? currentEntry(item.file, item.step.ref.entryId).entry : node_fs_1.default.existsSync(item.file) ? (0, proposal_queue_1.hashContent)(node_fs_1.default.readFileSync(item.file, 'utf8')) : null;
|
|
855
888
|
const expected = item.step ? item.step.after : item.action.delivery?.expectedHash;
|
|
856
|
-
|
|
857
|
-
|
|
889
|
+
const original = item.action.application?.reviewedContent;
|
|
890
|
+
if (actual !== expected && (item.step || typeof original !== 'string' || (0, proposal_queue_1.hashContent)(original) !== expected
|
|
891
|
+
|| !(0, run_publication_1.sameDeliveryText)(node_fs_1.default.readFileSync(item.file, 'utf8'), original)))
|
|
892
|
+
throw new Error(`completion-artifact-changed: ${item.file}`);
|
|
858
893
|
}
|
|
859
894
|
};
|
|
860
895
|
verifyLocal();
|
|
861
896
|
const finalEvidence = node_fs_1.default.existsSync(run.completionFile) ? node_fs_1.default.readFileSync(run.completionFile, 'utf8') : undefined;
|
|
862
|
-
const committed = finalEvidence &&
|
|
863
|
-
&& (0, run_record_1.
|
|
897
|
+
const committed = finalEvidence && state.finalization?.receipt?.finalizationState === 'complete'
|
|
898
|
+
&& (0, run_record_1.canonical)(run.readCompletion(finalEvidence)) === (0, run_record_1.canonical)(state.finalization.receipt);
|
|
864
899
|
for (const item of terminal.values()) {
|
|
865
900
|
if (!item.step) {
|
|
866
901
|
const proof = options.verifyCapability?.(item.action);
|
|
@@ -890,8 +925,10 @@ function completeRun(run, summary, homes, options, afterStage) {
|
|
|
890
925
|
guard.verify();
|
|
891
926
|
for (const item of terminal.values()) {
|
|
892
927
|
const actual = item.step ? currentEntry(item.file, item.step.ref.entryId).entry : node_fs_1.default.existsSync(item.file) ? (0, proposal_queue_1.hashContent)(node_fs_1.default.readFileSync(item.file, 'utf8')) : null;
|
|
893
|
-
if (
|
|
894
|
-
throw new Error(
|
|
928
|
+
if (item.step && actual !== item.step.after)
|
|
929
|
+
throw new Error(`completion-artifact-changed: ${item.file}`);
|
|
930
|
+
// Capability guards recheck exact local snapshots captured by the
|
|
931
|
+
// delivery verifier, including when approved text used other EOLs.
|
|
895
932
|
}
|
|
896
933
|
if (finalizing && !deferred)
|
|
897
934
|
completionInventory(run, homes, true);
|
|
@@ -908,11 +945,12 @@ function completionInventory(run, homes, removeFamilies = false) {
|
|
|
908
945
|
let recordedCleanup = state.cleanup;
|
|
909
946
|
if ('finalization' in state && state.finalization && node_fs_1.default.existsSync(run.completionFile)) {
|
|
910
947
|
const evidence = node_fs_1.default.readFileSync(run.completionFile, 'utf8');
|
|
911
|
-
if (![state.finalization.evidenceHash, state.finalization.previousEvidenceHash].includes((0, proposal_queue_1.hashContent)(evidence)))
|
|
912
|
-
throw new Error('completion-evidence-conflict');
|
|
913
948
|
if (evidence.includes('fraim-sleep-decisions')) {
|
|
914
|
-
const prior =
|
|
915
|
-
|
|
949
|
+
const prior = run.readCompletion(evidence);
|
|
950
|
+
// Legacy finalizations have no journaled receipt. Their narrative cannot
|
|
951
|
+
// introduce cleanup authority beyond the original journal inventory.
|
|
952
|
+
if (state.finalization.receipt)
|
|
953
|
+
recordedCleanup = [...prior.cleanup, ...recordedCleanup];
|
|
916
954
|
}
|
|
917
955
|
}
|
|
918
956
|
for (const item of recordedCleanup) {
|
|
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.sameDeliveryText = sameDeliveryText;
|
|
6
7
|
exports.publishRunFile = publishRunFile;
|
|
7
8
|
exports.verifyCapabilityDelivery = verifyCapabilityDelivery;
|
|
8
9
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
@@ -11,6 +12,10 @@ const node_child_process_1 = require("node:child_process");
|
|
|
11
12
|
const proposal_queue_1 = require("./proposal-queue");
|
|
12
13
|
const owner_identity_1 = require("./owner-identity");
|
|
13
14
|
const run_record_1 = require("./run-record");
|
|
15
|
+
/** Delivery permits CRLF/LF conversion only; approval identities and local guards keep exact hashes. */
|
|
16
|
+
function sameDeliveryText(actual, reviewed) {
|
|
17
|
+
return actual.replace(/\r\n/g, '\n') === reviewed.replace(/\r\n/g, '\n');
|
|
18
|
+
}
|
|
14
19
|
const { learningEntryBlocks } = require('./entry-format');
|
|
15
20
|
function matchesLearningPublication(body, proof) {
|
|
16
21
|
body = body.replace(/\r\n/g, '\n');
|
|
@@ -64,8 +69,8 @@ function verifySharedProjectPublication(projectPath, file, expectedHash, publica
|
|
|
64
69
|
const effective = (0, node_child_process_1.spawnSync)('git', ['show', `${effectiveHead}:${relative}`], { cwd: projectPath, encoding: 'utf8', timeout: 30000, windowsHide: true });
|
|
65
70
|
const current = node_fs_1.default.readFileSync(artifact.file, 'utf8');
|
|
66
71
|
if (delivered.status !== 0 || effective.status !== 0 || (0, proposal_queue_1.hashContent)(current) !== artifact.expectedHash
|
|
67
|
-
|| (
|
|
68
|
-
|| (
|
|
72
|
+
|| !sameDeliveryText(delivered.stdout, current)
|
|
73
|
+
|| !sameDeliveryText(effective.stdout, current))
|
|
69
74
|
return { published: false, reference: pr.url };
|
|
70
75
|
}
|
|
71
76
|
return { published: true, reference: pr.url };
|
|
@@ -101,7 +106,7 @@ function publishRunFile(projectPath, file, home, operationKey, expectedHash, run
|
|
|
101
106
|
const delivered = (0, node_child_process_1.spawnSync)('git', ['show', `${pr.mergeCommit.oid}:${packRelative}`], { cwd: home.root, encoding: 'utf8', timeout: 30000, windowsHide: true });
|
|
102
107
|
const current = node_fs_1.default.readFileSync(file, 'utf8');
|
|
103
108
|
const matches = (body) => entryProof ? matchesLearningPublication(body, entryProof)
|
|
104
|
-
: (
|
|
109
|
+
: sameDeliveryText(body, current);
|
|
105
110
|
if ((0, proposal_queue_1.hashContent)(current) !== expectedHash || fetched.status !== 0 || delivered.status !== 0 || !matches(delivered.stdout))
|
|
106
111
|
return { published: false, reference: pr.url };
|
|
107
112
|
// A merged snapshot proves this operation, but a later revert must
|
|
@@ -124,6 +129,7 @@ const [publisherPath,userConfigPath,file,relative,layer,branchSuffix,expectedHas
|
|
|
124
129
|
const entryProof=JSON.parse(entryProofJson);
|
|
125
130
|
const {learningEntryBlocks}=require(entryFormatPath);
|
|
126
131
|
const matchesLearningPublication=${matchesLearningPublication.toString()};
|
|
132
|
+
const sameDeliveryText=${sameDeliveryText.toString()};
|
|
127
133
|
const publisher=require(publisherPath);
|
|
128
134
|
const readConfig=require(userConfigPath).readUserFraimConfig;
|
|
129
135
|
const remote=(process.env.FRAIM_REMOTE_URL||'https://fraim.wellnessatwork.me').replace(/\\/$/,'');
|
|
@@ -133,7 +139,7 @@ async function confirmedCloud(){
|
|
|
133
139
|
if(!response.ok)throw Error('pack-read-failed');
|
|
134
140
|
const pack=await response.json();
|
|
135
141
|
const matches=Array.isArray(pack.files)?pack.files.filter(item=>item.relativePath===relative):[];
|
|
136
|
-
return {published:matches.length===1&&typeof matches[0].content==='string'&&(entryProof?matchesLearningPublication(matches[0].content,entryProof):
|
|
142
|
+
return {published:matches.length===1&&typeof matches[0].content==='string'&&(entryProof?matchesLearningPublication(matches[0].content,entryProof):sameDeliveryText(matches[0].content,fs.readFileSync(file,'utf8'))),reference:remote+'/api/'+layer+'/pack#'+encodeURIComponent(relative)};
|
|
137
143
|
}
|
|
138
144
|
(async()=>{
|
|
139
145
|
if(hash(fs.readFileSync(file,'utf8'))!==expectedHash)throw Error('publication-content-conflict');
|
|
@@ -179,9 +185,20 @@ async function verifyCapabilityDelivery(action, input, options) {
|
|
|
179
185
|
const target = (0, owner_identity_1.ownerArtifactPath)(home, action.target.relativePath);
|
|
180
186
|
if (!/^[a-f0-9]{64}$/.test(input?.expectedHash || '') || !Array.isArray(input.callers) || !node_fs_1.default.existsSync(target))
|
|
181
187
|
throw new Error('capability-verification-required');
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
188
|
+
const targetContent = node_fs_1.default.readFileSync(target, 'utf8');
|
|
189
|
+
const localHash = (0, proposal_queue_1.hashContent)(targetContent);
|
|
190
|
+
// Legacy approvals may bind mixed line endings. Supplied original bytes are
|
|
191
|
+
// evidence only: their exact hash must already be authorized by the review.
|
|
192
|
+
if (input.reviewedContent !== undefined && (typeof input.reviewedContent !== 'string'
|
|
193
|
+
|| (0, proposal_queue_1.hashContent)(input.reviewedContent) !== input.expectedHash))
|
|
194
|
+
throw new Error('capability-reviewed-content-mismatch');
|
|
195
|
+
if (localHash !== input.expectedHash && (input.reviewedContent === undefined || !sameDeliveryText(targetContent, input.reviewedContent))) {
|
|
196
|
+
return { published: false, reference: target, contentHash: localHash, reachable: false };
|
|
197
|
+
}
|
|
198
|
+
const contentHash = input.expectedHash;
|
|
199
|
+
// Preserve success evidence for later newline conversion, including an exact
|
|
200
|
+
// initial delivery. These bytes already match the immutable approved digest.
|
|
201
|
+
const reviewEvidence = { reviewedContent: input.reviewedContent ?? targetContent };
|
|
185
202
|
const load = (relative) => options.runtimeModule(options.projectPath, relative);
|
|
186
203
|
if (reader) {
|
|
187
204
|
const snapshots = new Map();
|
|
@@ -249,14 +266,14 @@ async function verifyCapabilityDelivery(action, input, options) {
|
|
|
249
266
|
throw new Error('capability-local-content-changed');
|
|
250
267
|
}
|
|
251
268
|
};
|
|
252
|
-
if (snapshots.get(target)?.sha256 !==
|
|
269
|
+
if (snapshots.get(target)?.sha256 !== localHash)
|
|
253
270
|
throw new Error('capability-local-content-changed');
|
|
254
271
|
verifySnapshots();
|
|
255
272
|
if (home.requiresPublish && (!options.runtimeModulePath || !input.operationKey))
|
|
256
273
|
throw new Error('capability-publication-required');
|
|
257
|
-
const publication = home.requiresPublish ? publishRunFile(options.projectPath, target, home, input.operationKey,
|
|
274
|
+
const publication = home.requiresPublish ? publishRunFile(options.projectPath, target, home, input.operationKey, localHash, options.runtimeModulePath) : { published: true, reference: target };
|
|
258
275
|
verifySnapshots();
|
|
259
|
-
return { ...publication, contentHash, reachable: true, localFiles };
|
|
276
|
+
return { ...publication, contentHash, reachable: true, localFiles, ...reviewEvidence };
|
|
260
277
|
}
|
|
261
278
|
const { LocalRegistryResolver } = load('core/utils/local-registry-resolver');
|
|
262
279
|
const { resolvePackHome } = load('cli/utils/pack-home');
|
|
@@ -271,7 +288,7 @@ async function verifyCapabilityDelivery(action, input, options) {
|
|
|
271
288
|
const snapshot = (file, expectedContent) => {
|
|
272
289
|
const absolute = node_path_1.default.resolve(file);
|
|
273
290
|
const body = node_fs_1.default.readFileSync(absolute, 'utf8');
|
|
274
|
-
if (expectedContent !== undefined && (
|
|
291
|
+
if (expectedContent !== undefined && !sameDeliveryText(body, expectedContent))
|
|
275
292
|
throw new Error('capability-local-content-changed');
|
|
276
293
|
const current = { file: absolute, realPath: node_fs_1.default.realpathSync(absolute), sha256: (0, proposal_queue_1.hashContent)(body) };
|
|
277
294
|
const prior = snapshots.get(absolute);
|
|
@@ -300,16 +317,18 @@ async function verifyCapabilityDelivery(action, input, options) {
|
|
|
300
317
|
node_path_1.default.join(options.projectPath, 'fraim/ai-employee', normalized), node_path_1.default.join(options.projectPath, 'fraim/ai-manager', normalized),
|
|
301
318
|
node_path_1.default.join(options.projectPath, 'fraim', normalized), node_path_1.default.join(options.projectPath, 'registry', normalized)];
|
|
302
319
|
}
|
|
303
|
-
const source = candidates.find(file => node_fs_1.default.existsSync(file) && node_fs_1.default.statSync(file).isFile() && (
|
|
320
|
+
const source = candidates.find(file => node_fs_1.default.existsSync(file) && node_fs_1.default.statSync(file).isFile() && sameDeliveryText(node_fs_1.default.readFileSync(file, 'utf8'), resolved.content));
|
|
304
321
|
if (source)
|
|
305
322
|
snapshot(source, resolved.content);
|
|
306
323
|
};
|
|
307
|
-
snapshot(target);
|
|
324
|
+
snapshot(target, targetContent);
|
|
325
|
+
if (snapshots.get(node_path_1.default.resolve(target))?.sha256 !== localHash)
|
|
326
|
+
throw new Error('capability-local-content-changed');
|
|
308
327
|
const shared = action.target.owner.level === 'Project' && action.target.relativePath.startsWith('registry/');
|
|
309
328
|
const targetPath = shared ? action.target.relativePath.slice('registry/'.length) : action.target.relativePath;
|
|
310
329
|
const targetScope = shared ? undefined : action.target.owner.level.toLowerCase();
|
|
311
330
|
const loadedTarget = await originalResolve(targetPath, { includeMetadata: false, stripMcpHeader: true });
|
|
312
|
-
if (loadedTarget.scope !== targetScope || (
|
|
331
|
+
if (loadedTarget.scope !== targetScope || !sameDeliveryText(loadedTarget.content, targetContent))
|
|
313
332
|
return { published: false, reference: target, contentHash, reachable: false };
|
|
314
333
|
let reachable = true;
|
|
315
334
|
const sharedCallers = [];
|
|
@@ -321,7 +340,7 @@ async function verifyCapabilityDelivery(action, input, options) {
|
|
|
321
340
|
const callerPath = sharedCaller ? caller.relativePath.slice('registry/'.length) : caller.relativePath;
|
|
322
341
|
const loadedCaller = await originalResolve(callerPath, { includeMetadata: false, stripMcpHeader: true });
|
|
323
342
|
if (!node_fs_1.default.existsSync(callerFile) || loadedCaller.scope !== (sharedCaller ? undefined : caller.owner.level.toLowerCase())
|
|
324
|
-
|| (
|
|
343
|
+
|| !sameDeliveryText(loadedCaller.content, node_fs_1.default.readFileSync(callerFile, 'utf8'))) {
|
|
325
344
|
reachable = false;
|
|
326
345
|
break;
|
|
327
346
|
}
|
|
@@ -332,7 +351,7 @@ async function verifyCapabilityDelivery(action, input, options) {
|
|
|
332
351
|
resolver.resolveFile = async (relative, resolveOptions) => {
|
|
333
352
|
const resolved = await originalResolve(relative, resolveOptions);
|
|
334
353
|
snapshotResolvedInclude(relative, resolved);
|
|
335
|
-
if (relative === targetPath && resolved.scope === targetScope && (
|
|
354
|
+
if (relative === targetPath && resolved.scope === targetScope && sameDeliveryText(resolved.content, targetContent))
|
|
336
355
|
traversed = true;
|
|
337
356
|
return resolved;
|
|
338
357
|
};
|
|
@@ -343,7 +362,7 @@ async function verifyCapabilityDelivery(action, input, options) {
|
|
|
343
362
|
}
|
|
344
363
|
}
|
|
345
364
|
resolver.resolveFile = originalResolve;
|
|
346
|
-
if (!reachable || (0, proposal_queue_1.hashContent)(node_fs_1.default.readFileSync(target, 'utf8')) !==
|
|
365
|
+
if (!reachable || (0, proposal_queue_1.hashContent)(node_fs_1.default.readFileSync(target, 'utf8')) !== localHash)
|
|
347
366
|
return { published: false, reference: target, contentHash, reachable: false };
|
|
348
367
|
const localFiles = [...snapshots.values()].sort((a, b) => a.file.localeCompare(b.file));
|
|
349
368
|
const verifySnapshots = () => {
|
|
@@ -352,13 +371,13 @@ async function verifyCapabilityDelivery(action, input, options) {
|
|
|
352
371
|
throw new Error('capability-local-content-changed');
|
|
353
372
|
};
|
|
354
373
|
if (shared) {
|
|
355
|
-
const publication = verifySharedProjectPublication(options.projectPath, target,
|
|
374
|
+
const publication = verifySharedProjectPublication(options.projectPath, target, localHash, input.publicationRef, sharedCallers);
|
|
356
375
|
verifySnapshots();
|
|
357
|
-
return { ...publication, contentHash, reachable, localFiles };
|
|
376
|
+
return { ...publication, contentHash, reachable, localFiles, ...reviewEvidence };
|
|
358
377
|
}
|
|
359
378
|
if (home.requiresPublish && (!options.runtimeModulePath || !input.operationKey))
|
|
360
379
|
throw new Error('capability-publication-required');
|
|
361
|
-
const publication = home.requiresPublish ? publishRunFile(options.projectPath, target, home, input.operationKey,
|
|
380
|
+
const publication = home.requiresPublish ? publishRunFile(options.projectPath, target, home, input.operationKey, localHash, options.runtimeModulePath) : { published: true, reference: target };
|
|
362
381
|
verifySnapshots();
|
|
363
|
-
return { ...publication, contentHash, reachable, localFiles };
|
|
382
|
+
return { ...publication, contentHash, reachable, localFiles, ...reviewEvidence };
|
|
364
383
|
}
|
|
@@ -49,8 +49,8 @@ function bindCleanupItem(item, home) {
|
|
|
49
49
|
return { ...item, path: file, location: { owner: home.owner, relativePath } };
|
|
50
50
|
}
|
|
51
51
|
function sameCleanupIdentity(a, b) {
|
|
52
|
-
return node_path_1.default.resolve(a.path) === node_path_1.default.resolve(b.path)
|
|
53
|
-
&& (0, completion_state_1.canonicalJournal)(a.location) === (0, completion_state_1.canonicalJournal)(b.location) && a.
|
|
52
|
+
return node_path_1.default.resolve(a.path) === node_path_1.default.resolve(b.path) && a.sha256 === b.sha256
|
|
53
|
+
&& (0, completion_state_1.canonicalJournal)(a.location) === (0, completion_state_1.canonicalJournal)(b.location) && a.reason === b.reason;
|
|
54
54
|
}
|
|
55
55
|
function validateDeliveryReader(reader) { validateReader(reader, false); }
|
|
56
56
|
function validateReader(reader, historical) {
|
|
@@ -134,6 +134,47 @@ function reviewedInput(input) {
|
|
|
134
134
|
result[key] = structuredClone(input[key]);
|
|
135
135
|
return result;
|
|
136
136
|
}
|
|
137
|
+
/** New authored text only: runtime owner/source bindings and historical replay keep their existing contract. */
|
|
138
|
+
function assertPortableAuthoredText(value, previous) {
|
|
139
|
+
if (value === previous)
|
|
140
|
+
return;
|
|
141
|
+
if (typeof value === 'string') {
|
|
142
|
+
// Absolute API routes and filesystem paths share a syntax. Only an explicit
|
|
143
|
+
// HTTP-method context disambiguates a route; bare local-looking paths must
|
|
144
|
+
// use an owner-relative reference. No filesystem directory allowlist.
|
|
145
|
+
const text = value.replace(/https?:\/\/[^\s<>"'`]+/gi, url => {
|
|
146
|
+
try {
|
|
147
|
+
return new URL(url).hostname ? 'URL' : url;
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return url;
|
|
151
|
+
}
|
|
152
|
+
}).replace(/\b(?:GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS|CONNECT|TRACE)[ \t]+([`"']?)(\/(?!\/)[^\s<>"'`]+)\1/g, 'HTTP route')
|
|
153
|
+
.replace(/<\/[A-Za-z][A-Za-z0-9:._-]*\s*>/g, 'closing tag')
|
|
154
|
+
.replace(/(<[A-Za-z][A-Za-z0-9:._-]*(?:\s+[^<>]*?)?)\/>/g, '$1>');
|
|
155
|
+
if (/\bfile:(?=\S)/i.test(text)
|
|
156
|
+
|| /(?:\$(?:HOME|USERPROFILE)|\$\{(?:HOME|USERPROFILE)\}|%USERPROFILE%|\$env:USERPROFILE)[/\x5c]/i.test(text)
|
|
157
|
+
|| /(?:^|[^\p{L}\p{N}_./\x5c-])(?:[A-Za-z]:[/\x5c]|\x5c{2}|\/\/(?=\S)|~(?:[/\x5c]|[\p{L}\p{N}_.-]+[/\x5c])|\/(?![\s/*]))/u.test(text)) {
|
|
158
|
+
throw new Error('machine-local-path-in-authored-text');
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
else if (Array.isArray(value)) {
|
|
162
|
+
value.forEach((item, index) => assertPortableAuthoredText(item, Array.isArray(previous) ? previous[index] : undefined));
|
|
163
|
+
}
|
|
164
|
+
else if (value && typeof value === 'object') {
|
|
165
|
+
for (const [key, item] of Object.entries(value)) {
|
|
166
|
+
assertPortableAuthoredText(key);
|
|
167
|
+
assertPortableAuthoredText(item, previous && typeof previous === 'object' ? previous[key] : undefined);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function assertPortableActionText(input, previous) {
|
|
172
|
+
// sourceRefs/sourceHashes are physical IO bindings in the existing journal
|
|
173
|
+
// schema. Authored entry trace references are part of content and are checked.
|
|
174
|
+
for (const key of ['claimKey', 'evidenceFingerprint', 'content', 'specification', 'verification']) {
|
|
175
|
+
assertPortableAuthoredText(input[key], previous?.[key]);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
137
178
|
function validateAction(action, historical = false) {
|
|
138
179
|
if (!['learning', 'capability'].includes(action.kind) || !['insert', 'update', 'move', 'remove', 'deliver', 'reconcile'].includes(action.operation))
|
|
139
180
|
throw new Error('invalid-action');
|
|
@@ -206,6 +247,9 @@ function validateAction(action, historical = false) {
|
|
|
206
247
|
function validateApplication(action, application) {
|
|
207
248
|
if (!application || !['prepared', 'written', 'published', 'applied'].includes(application.stage) || !Array.isArray(application.steps))
|
|
208
249
|
throw new Error('invalid-application');
|
|
250
|
+
if (application.reviewedContent !== undefined && (action.kind !== 'capability' || typeof application.reviewedContent !== 'string'
|
|
251
|
+
|| (0, proposal_queue_1.hashContent)(application.reviewedContent) !== action.delivery?.expectedHash))
|
|
252
|
+
throw new Error('capability-reviewed-content-mismatch');
|
|
209
253
|
const refs = action.kind === 'learning' ? [action.target, ...(action.operation === 'move' ? [action.source] : action.copies?.map(copy => copy.ref) || [])] : [];
|
|
210
254
|
if (application.steps.length !== refs.length)
|
|
211
255
|
throw new Error('invalid-application-targets');
|
|
@@ -343,7 +387,9 @@ function replay(file) {
|
|
|
343
387
|
if (receipt.decisions.length !== completed.length || receipt.decisions.some(item => !completed.some(action => action.actionId === item.actionId
|
|
344
388
|
&& (0, completion_state_1.canonicalJournal)(action.decision) === (0, completion_state_1.canonicalJournal)(item.decision) && (0, completion_state_1.canonicalJournal)(action.target) === (0, completion_state_1.canonicalJournal)(item.target))))
|
|
345
389
|
throw new Error('checkpoint-action-conflict');
|
|
346
|
-
|
|
390
|
+
const omitted = new Set(receipt.deferred.sources.map(source => (0, completion_state_1.sourcePathKey)(source.path)));
|
|
391
|
+
if (state.archivedSourcePaths?.some(file => omitted.has(file))
|
|
392
|
+
|| (0, completion_state_1.canonicalJournal)(receipt.processedSources) !== (0, completion_state_1.canonicalJournal)(state.processedSources?.filter(source => !omitted.has((0, completion_state_1.sourcePathKey)(source.path)))))
|
|
347
393
|
throw new Error('completion-sources-conflict');
|
|
348
394
|
if ((0, completion_state_1.canonicalJournal)(receipt.outcomeAssessments) !== (0, completion_state_1.canonicalJournal)(state.outcomeAssessments))
|
|
349
395
|
throw new Error('completion-assessments-conflict');
|
|
@@ -362,6 +408,9 @@ function replay(file) {
|
|
|
362
408
|
}
|
|
363
409
|
else
|
|
364
410
|
throw new Error('unknown-journal-event');
|
|
411
|
+
if (['checkpoint', 'finalization'].includes(type) && data.receipt?.finalizationState === 'complete') {
|
|
412
|
+
state.archivedSourcePaths = [...new Set([...(state.archivedSourcePaths ?? []), ...(data.receipt.processedSources ?? []).map((source) => (0, completion_state_1.sourcePathKey)(source.path))])];
|
|
413
|
+
}
|
|
365
414
|
state.sequence = event.sequence;
|
|
366
415
|
}
|
|
367
416
|
if (!state)
|
|
@@ -419,6 +468,26 @@ class RunRecord {
|
|
|
419
468
|
this.assertCompletionDirectory(state);
|
|
420
469
|
return state;
|
|
421
470
|
}
|
|
471
|
+
/** Narrative is editable; the journal owns the receipt and its interrupted replacement. */
|
|
472
|
+
readCompletion(body) {
|
|
473
|
+
const receipt = (0, completion_state_1.decodeCompletion)(body, { runId: this.runId, owner: this.owner });
|
|
474
|
+
if (!node_fs_1.default.existsSync(this.file))
|
|
475
|
+
return receipt;
|
|
476
|
+
const { events } = replay(this.file);
|
|
477
|
+
const bindings = events.filter(event => ['checkpoint', 'finalization'].includes(event.payload.type));
|
|
478
|
+
const latest = bindings.at(-1)?.payload.data;
|
|
479
|
+
if (latest?.receipt) {
|
|
480
|
+
// Older checkpoints already stored their authoritative receipt. Read the
|
|
481
|
+
// preceding event only for a crash between replacement intent and rename.
|
|
482
|
+
const previous = latest.previousReceipt ?? bindings.at(-2)?.payload.data.receipt;
|
|
483
|
+
if (![latest.receipt, previous].some(value => value && (0, completion_state_1.canonicalJournal)(value) === (0, completion_state_1.canonicalJournal)(receipt))) {
|
|
484
|
+
throw new Error(`completion-receipt-conflict: ${this.completionFile}: structured receipt differs from recorded decisions or source mappings`);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
// Legacy finalization events recorded no receipt. The full-completion path
|
|
488
|
+
// reconstructs decisions from the journal and independently inventories cleanup.
|
|
489
|
+
return receipt;
|
|
490
|
+
}
|
|
422
491
|
assertCompletionDirectory(state) {
|
|
423
492
|
if ((0, completion_state_1.sourcePathKey)(state.completionDirectory ?? this.directory) !== (0, completion_state_1.sourcePathKey)(this.completionDirectory))
|
|
424
493
|
throw new Error('completion-evidence-path-changed');
|
|
@@ -472,6 +541,7 @@ class RunRecord {
|
|
|
472
541
|
throw new Error('run-context-mismatch');
|
|
473
542
|
if ([...(state.outcomeAssessments ?? []), ...(state.pendingOutcomeAssessments ?? [])].some(item => item.digest === record.digest))
|
|
474
543
|
return;
|
|
544
|
+
assertPortableAuthoredText([record.incidentId, record.rationale]);
|
|
475
545
|
this.append('assessment-prepared', { record, sourcePaths });
|
|
476
546
|
});
|
|
477
547
|
}
|
|
@@ -494,6 +564,7 @@ class RunRecord {
|
|
|
494
564
|
const prior = Object.values(state.actions).find(a => a.claimKey === input.claimKey && a.evidenceFingerprint === input.evidenceFingerprint && (0, owner_identity_1.ownerKey)(a.target.owner) === (0, owner_identity_1.ownerKey)(input.target.owner));
|
|
495
565
|
if (prior)
|
|
496
566
|
return prior;
|
|
567
|
+
assertPortableActionText(input);
|
|
497
568
|
for (const name of node_fs_1.default.readdirSync(this.directory).filter(n => n.endsWith('-completion.md'))) {
|
|
498
569
|
const history = node_fs_1.default.readFileSync(node_path_1.default.join(this.directory, name), 'utf8');
|
|
499
570
|
let completed;
|
|
@@ -523,6 +594,7 @@ class RunRecord {
|
|
|
523
594
|
throw new Error('action-completed');
|
|
524
595
|
if (prior.application)
|
|
525
596
|
throw new Error('application-started');
|
|
597
|
+
assertPortableActionText(input, prior);
|
|
526
598
|
if (prior.content && input.content && (0, completion_state_1.canonicalJournal)(prior.content.trace) !== (0, completion_state_1.canonicalJournal)(input.content.trace))
|
|
527
599
|
throw new Error('immutable-history');
|
|
528
600
|
if (prior.target.entryId !== input.target.entryId)
|
|
@@ -554,6 +626,7 @@ class RunRecord {
|
|
|
554
626
|
throw new Error('action-completed');
|
|
555
627
|
if (action.application || decision.reviewer !== state.reviewer || typeof decision.reason !== 'string' || !decision.reason.trim() || !['approve', 'reject'].includes(decision.action) || (decision.revisitCondition !== undefined && (decision.action !== 'reject' || typeof decision.revisitCondition !== 'string' || !decision.revisitCondition.trim())) || !validDecisionProvenance(decision))
|
|
556
628
|
throw new Error('invalid-decision');
|
|
629
|
+
assertPortableAuthoredText([decision.reason, decision.revisitCondition, decision.predecessorId]);
|
|
557
630
|
this.append('decision', { actionId, decision: { ...decision, revision, digest: action.digest, at: new Date().toISOString() } });
|
|
558
631
|
return this.read().actions[actionId];
|
|
559
632
|
});
|
|
@@ -562,6 +635,7 @@ class RunRecord {
|
|
|
562
635
|
return this.mutate(expectedSequence, state => {
|
|
563
636
|
if (reviewer !== state.reviewer || !reason.trim() || !tuples.length || new Set(tuples.map(t => t.actionId)).size !== tuples.length)
|
|
564
637
|
throw new Error('invalid-batch-decision');
|
|
638
|
+
assertPortableAuthoredText(reason);
|
|
565
639
|
for (const tuple of tuples) {
|
|
566
640
|
const action = state.actions[tuple.actionId];
|
|
567
641
|
if (!action || action.application || action.revision !== tuple.revision || action.digest !== tuple.digest)
|
|
@@ -613,6 +687,7 @@ class RunRecord {
|
|
|
613
687
|
this.append('application', { actionId, digest: action.digest, application });
|
|
614
688
|
}
|
|
615
689
|
recordCleanup(item, expectedSequence) {
|
|
690
|
+
assertPortableAuthoredText(item.reason);
|
|
616
691
|
// The normal run location supplies an unambiguous same-owner home for
|
|
617
692
|
// direct registrations. Cross-owner reconciliations bind their resolved home explicitly.
|
|
618
693
|
if (!item.location) {
|
|
@@ -648,6 +723,7 @@ class RunRecord {
|
|
|
648
723
|
}),
|
|
649
724
|
};
|
|
650
725
|
validateReconciliation(reconciliation, state);
|
|
726
|
+
assertPortableAuthoredText([input.rationale, input.preservedDecisions]);
|
|
651
727
|
this.append('reconciliation', { reconciliation, cleanup });
|
|
652
728
|
return this.read();
|
|
653
729
|
});
|
|
@@ -680,6 +756,7 @@ class RunRecord {
|
|
|
680
756
|
return { actionId: id, revision: action.revision, digest: action.digest };
|
|
681
757
|
});
|
|
682
758
|
(0, corpus_audit_1.validateCorpusAudit)(audit, state.reviewer);
|
|
759
|
+
assertPortableAuthoredText(audit.dispositions.map(item => item.rationale));
|
|
683
760
|
this.append('corpus-audit', audit);
|
|
684
761
|
return this.read();
|
|
685
762
|
});
|
|
@@ -710,7 +787,7 @@ class RunRecord {
|
|
|
710
787
|
const result = (0, completion_state_1.decodeCompletion)(node_fs_1.default.readFileSync(this.completionFile, 'utf8'), { runId: this.runId, owner: this.owner });
|
|
711
788
|
if (result.finalizationState !== 'complete')
|
|
712
789
|
throw new Error('incomplete-finalization');
|
|
713
|
-
if (
|
|
790
|
+
if (result.retrospective?.relativePath !== retrospective?.relativePath)
|
|
714
791
|
throw new Error('completion-retrospective-conflict');
|
|
715
792
|
verifyApplied?.(true);
|
|
716
793
|
finishedInventory();
|
|
@@ -721,9 +798,11 @@ class RunRecord {
|
|
|
721
798
|
throw new Error('assessment-processing-pending');
|
|
722
799
|
if (!summary?.trim() || summary.includes('fraim-sleep-decisions'))
|
|
723
800
|
throw new Error('invalid-completion-summary');
|
|
801
|
+
assertPortableAuthoredText(summary);
|
|
724
802
|
const actions = deferred ? completedRunActions(state) : Object.values(state.actions);
|
|
725
803
|
if (deferred) {
|
|
726
804
|
(0, completion_state_1.validateDeferredWork)(deferred);
|
|
805
|
+
assertPortableAuthoredText([...deferred.actions, ...deferred.sources, ...(deferred.corpusErrors ?? [])].map(item => item.reason));
|
|
727
806
|
const ids = new Set(actions.map(action => action.actionId));
|
|
728
807
|
if ((0, completion_state_1.canonicalJournal)(deferred.actions.map(item => [item.actionId, item.revision, item.digest])) !== (0, completion_state_1.canonicalJournal)(Object.values(state.actions).filter(action => !ids.has(action.actionId)).map(action => [action.actionId, action.revision, action.digest])))
|
|
729
808
|
throw new Error('checkpoint-action-conflict');
|
|
@@ -751,7 +830,7 @@ class RunRecord {
|
|
|
751
830
|
decisions: actions.map(a => ({ actionId: a.actionId, revision: a.revision, digest: a.digest, owner: a.target.owner, target: a.target, claimKey: a.claimKey, evidenceFingerprint: a.evidenceFingerprint, sourceRefs: a.sourceRefs, decision: a.decision, dependencies: a.dependencies.map(id => ({ actionId: id, outcome: state.actions[id].decision?.action === 'approve' ? 'applied' : 'rejected' })), ...(a.application?.publicationRef ? { publicationRef: a.application.publicationRef } : {}) })),
|
|
752
831
|
sourceDispositions: actions.flatMap(a => a.sourceRefs.map(sourceRef => ({ sourceRef, sourceHash: a.sourceHashes[sourceRef], outcome: a.decision.action === 'approve' ? 'applied' : 'rejected', actionId: a.actionId }))),
|
|
753
832
|
cleanup: snapshot,
|
|
754
|
-
...(state.processedSources ? { processedSources: state.processedSources } : {}),
|
|
833
|
+
...(state.processedSources ? { processedSources: state.processedSources.filter(source => !deferred?.sources.some(item => (0, completion_state_1.sourcePathKey)(item.path) === (0, completion_state_1.sourcePathKey)(source.path))) } : {}),
|
|
755
834
|
...(state.sourceParticipants !== undefined ? { sourceParticipants: state.sourceParticipants } : {}),
|
|
756
835
|
...(state.outcomeAssessments?.length ? { outcomeAssessments: state.outcomeAssessments } : {}),
|
|
757
836
|
...(retrospective ? { retrospective } : {}),
|
|
@@ -767,6 +846,8 @@ class RunRecord {
|
|
|
767
846
|
// Submission and feedback already own this path before the completion receipt exists.
|
|
768
847
|
const existingEvidence = node_fs_1.default.existsSync(this.completionFile) ? node_fs_1.default.readFileSync(this.completionFile, 'utf8') : undefined;
|
|
769
848
|
const hasReceipt = existingEvidence?.includes('fraim-sleep-decisions');
|
|
849
|
+
if (!hasReceipt && (state.checkpoint || state.finalization))
|
|
850
|
+
throw new Error(`completion-receipt-conflict: ${this.completionFile}: recorded receipt is missing`);
|
|
770
851
|
if (existingEvidence && !hasReceipt) {
|
|
771
852
|
const header = existingEvidence.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
|
|
772
853
|
if (!header || (header[1].match(/^artifactKind:/gm) || []).length !== 1 || !/^artifactKind: work-evidence\s*$/m.test(header[1])
|
|
@@ -775,46 +856,73 @@ class RunRecord {
|
|
|
775
856
|
narrative = existingEvidence + `\n\n## Sleep completion: ${this.runId}\n\n${summary}\n\n## Decisions and source disposition\n\n`;
|
|
776
857
|
}
|
|
777
858
|
let trailingHistory = '\n';
|
|
859
|
+
let observedEvidence = existingEvidence;
|
|
778
860
|
const writeEvidence = () => {
|
|
861
|
+
const currentBody = node_fs_1.default.existsSync(this.completionFile) ? node_fs_1.default.readFileSync(this.completionFile, 'utf8') : undefined;
|
|
862
|
+
if (currentBody !== observedEvidence) {
|
|
863
|
+
if (!currentBody?.includes('fraim-sleep-decisions'))
|
|
864
|
+
throw new Error(`completion-receipt-conflict: ${this.completionFile}: receipt removed during completion`);
|
|
865
|
+
this.readCompletion(currentBody);
|
|
866
|
+
narrative = currentBody.slice(0, currentBody.indexOf('<!-- fraim-sleep-decisions:v1 -->'));
|
|
867
|
+
trailingHistory = currentBody.slice(currentBody.indexOf('<!-- /fraim-sleep-decisions -->') + '<!-- /fraim-sleep-decisions -->'.length);
|
|
868
|
+
}
|
|
779
869
|
if (actions.length)
|
|
780
870
|
narrative = narrative.replace(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/, header => header.replace(/^containsFeedback: false[ \t]*(?=\r?$)/m, 'containsFeedback: true'));
|
|
781
871
|
const encoded = JSON.stringify(result, null, 2).split('<').join(String.raw `\u003c`);
|
|
782
872
|
const body = narrative + `<!-- fraim-sleep-decisions:v1 -->\n\x60\x60\x60json\n${encoded}\n\x60\x60\x60\n<!-- /fraim-sleep-decisions -->` + trailingHistory;
|
|
783
873
|
(0, completion_state_1.decodeCompletion)(body, { runId: this.runId, owner: this.owner });
|
|
784
|
-
const
|
|
874
|
+
const previousBody = node_fs_1.default.existsSync(this.completionFile) ? node_fs_1.default.readFileSync(this.completionFile, 'utf8') : undefined;
|
|
875
|
+
const previousEvidenceHash = previousBody === undefined ? undefined : (0, proposal_queue_1.hashContent)(previousBody);
|
|
876
|
+
const previousReceipt = previousBody?.includes('fraim-sleep-decisions') ? this.readCompletion(previousBody) : undefined;
|
|
785
877
|
// A replacement intent names both recoverable states. A crash before
|
|
786
878
|
// or after the atomic rename can resume without accepting a third state.
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
this.append(
|
|
879
|
+
const event = deferred ? 'checkpoint' : 'finalization';
|
|
880
|
+
const binding = { evidenceHash: (0, proposal_queue_1.hashContent)(body), previousEvidenceHash, receipt: result, previousReceipt };
|
|
881
|
+
if (previousReceipt)
|
|
882
|
+
this.append(event, binding);
|
|
791
883
|
(0, proposal_queue_1.atomicWrite)(this.completionFile, body);
|
|
884
|
+
observedEvidence = body;
|
|
792
885
|
const verified = node_fs_1.default.readFileSync(this.completionFile, 'utf8');
|
|
793
886
|
if ((0, proposal_queue_1.hashContent)(verified) !== (0, proposal_queue_1.hashContent)(body))
|
|
794
887
|
throw new Error('completion-write-conflict');
|
|
795
888
|
(0, completion_state_1.decodeCompletion)(verified, { runId: this.runId, owner: this.owner });
|
|
796
|
-
if (!
|
|
797
|
-
this.append(
|
|
889
|
+
if (!previousReceipt)
|
|
890
|
+
this.append(event, binding);
|
|
798
891
|
};
|
|
799
892
|
if (node_fs_1.default.existsSync(this.completionFile) && hasReceipt) {
|
|
800
893
|
const body = node_fs_1.default.readFileSync(this.completionFile, 'utf8');
|
|
801
|
-
const prior =
|
|
802
|
-
if (!deferred && !prior.deferred && prior.finalizationState === 'complete' &&
|
|
894
|
+
const prior = this.readCompletion(body);
|
|
895
|
+
if (!deferred && !prior.deferred && prior.finalizationState === 'complete' && prior.retrospective?.relativePath !== retrospective?.relativePath)
|
|
803
896
|
throw new Error('completion-retrospective-conflict');
|
|
804
|
-
if (
|
|
805
|
-
|
|
897
|
+
if (deferred && !state.checkpoint && !state.finalization?.receipt) {
|
|
898
|
+
// A legacy full receipt has no structured journal binding. Prove its
|
|
899
|
+
// original full state before partitioning an interrupted source batch.
|
|
900
|
+
const inventory = inspect();
|
|
901
|
+
if (prior.cleanup.some(item => !inventory.some(current => sameCleanupIdentity(item, current)))
|
|
902
|
+
|| inventory.some(item => !prior.cleanup.some(current => sameCleanupIdentity(item, current)))
|
|
903
|
+
|| prior.cleanup.some(item => ['deleted', 'absent'].includes(item.outcome) && inventory.some(current => sameCleanupIdentity(item, current) && current.outcome === 'pending')))
|
|
904
|
+
throw new Error('cleanup-inventory-missing');
|
|
905
|
+
const full = { ...result, finalizationState: 'pending', deferred: undefined, cleanup: prior.cleanup,
|
|
906
|
+
processedSources: state.processedSources, reconciliations: state.reconciliations.length ? state.reconciliations : undefined,
|
|
907
|
+
corpusAudit: state.corpusAudit, reconciliationHistory: reconciliationHistory.length ? reconciliationHistory : undefined,
|
|
908
|
+
deferredHistory: state.deferredHistory?.length ? state.deferredHistory : undefined,
|
|
909
|
+
deferredAttempts: state.deferredAttempts?.length ? state.deferredAttempts : undefined };
|
|
910
|
+
if (prior.finalizationState !== 'pending' || (0, completion_state_1.canonicalJournal)(prior) !== (0, completion_state_1.canonicalJournal)(full))
|
|
911
|
+
throw new Error(`completion-decisions-conflict: ${this.completionFile}: legacy receipt differs from original full journal state`);
|
|
912
|
+
}
|
|
806
913
|
if (deferred || prior.deferred) {
|
|
807
|
-
if (
|
|
808
|
-
|| prior.
|
|
809
|
-
|
|
914
|
+
if (prior.decisions.some(item => !result.decisions.some(current => (0, completion_state_1.canonicalJournal)(current) === (0, completion_state_1.canonicalJournal)(item)))
|
|
915
|
+
|| prior.processedSources?.some(item => !result.processedSources?.some(current => (0, completion_state_1.canonicalJournal)(current) === (0, completion_state_1.canonicalJournal)(item))
|
|
916
|
+
&& (prior.finalizationState === 'complete' || !deferred?.sources.some(source => (0, completion_state_1.sourcePathKey)(source.path) === (0, completion_state_1.sourcePathKey)(item.path)))))
|
|
810
917
|
throw new Error('completion-decisions-conflict');
|
|
811
918
|
}
|
|
812
|
-
else if (
|
|
813
|
-
|
|
814
|
-
|
|
919
|
+
else if (['decisions', 'sourceDispositions', 'reconciliations', 'processedSources', 'corpusAudit', 'reconciliationHistory', 'outcomeAssessments', 'sourceParticipants', 'deferredHistory', 'deferredAttempts']
|
|
920
|
+
.some(key => (0, completion_state_1.canonicalJournal)(prior[key]) !== (0, completion_state_1.canonicalJournal)(result[key])))
|
|
921
|
+
throw new Error(`completion-decisions-conflict: ${this.completionFile}: receipt differs from journaled decisions or source history`);
|
|
922
|
+
if (deferred && state.checkpoint && prior.finalizationState === 'complete' && (0, completion_state_1.canonicalJournal)({ ...prior, finalizationState: 'pending' }) === (0, completion_state_1.canonicalJournal)(result)) {
|
|
815
923
|
finishSources?.();
|
|
816
924
|
verifyApplied?.();
|
|
817
|
-
this.append('deferred-attempt', { at: new Date().toISOString(), evidenceHash:
|
|
925
|
+
this.append('deferred-attempt', { at: new Date().toISOString(), evidenceHash: state.checkpoint.evidenceHash, reason: 'verified-subset-unchanged' });
|
|
818
926
|
return prior;
|
|
819
927
|
}
|
|
820
928
|
// While pending, the agent may update process evidence and the
|
|
@@ -824,26 +932,28 @@ class RunRecord {
|
|
|
824
932
|
if (prior.deferred && (!deferred || (0, completion_state_1.canonicalJournal)({ ...prior, finalizationState: 'pending' }) !== (0, completion_state_1.canonicalJournal)(result)))
|
|
825
933
|
narrative += `\n## Recovery completion\n\n${summary}\n\n`;
|
|
826
934
|
trailingHistory = body.slice(body.indexOf('<!-- /fraim-sleep-decisions -->') + '<!-- /fraim-sleep-decisions -->'.length);
|
|
827
|
-
if (!deferred && !prior.deferred && prior.finalizationState === 'complete' && !snapshot.some(item => item.outcome === 'pending')) {
|
|
828
|
-
finishedInventory();
|
|
829
|
-
finishSources?.();
|
|
830
|
-
verifyApplied?.(true);
|
|
831
|
-
node_fs_1.default.unlinkSync(this.file);
|
|
832
|
-
return prior;
|
|
833
|
-
}
|
|
834
|
-
if (!deferred && !prior.deferred && prior.finalizationState === 'complete' && snapshot.some(item => item.outcome === 'pending'))
|
|
835
|
-
throw new Error('cleanup-content-reappeared');
|
|
836
935
|
// The fresh inventory authorizes every remaining deletion. Never
|
|
837
936
|
// execute a path merely because it appeared in prior evidence.
|
|
937
|
+
if (!deferred && !prior.deferred && snapshot.some(current => !prior.cleanup.some(item => sameCleanupIdentity(item, current))))
|
|
938
|
+
throw new Error(`cleanup-inventory-missing: ${this.completionFile}: receipt omitted recorded cleanup`);
|
|
838
939
|
for (const item of deferred ? [] : prior.cleanup) {
|
|
839
940
|
const found = snapshot.find(current => sameCleanupIdentity(item, current));
|
|
840
941
|
if (!found)
|
|
841
942
|
throw new Error('cleanup-inventory-missing');
|
|
842
943
|
if ((item.outcome === 'deleted' || item.outcome === 'absent') && found.outcome === 'pending')
|
|
843
944
|
throw new Error('cleanup-content-reappeared');
|
|
844
|
-
if (found.outcome === 'absent' && item.outcome === 'deleted')
|
|
945
|
+
if (state.finalization?.receipt && found.outcome === 'absent' && item.outcome === 'deleted')
|
|
845
946
|
found.outcome = 'deleted';
|
|
846
947
|
}
|
|
948
|
+
if (!deferred && !prior.deferred && state.finalization?.receipt && prior.finalizationState === 'complete' && !snapshot.some(item => item.outcome === 'pending') && (0, completion_state_1.canonicalJournal)(prior.retrospective) === (0, completion_state_1.canonicalJournal)(retrospective)) {
|
|
949
|
+
finishedInventory();
|
|
950
|
+
finishSources?.();
|
|
951
|
+
verifyApplied?.(true);
|
|
952
|
+
node_fs_1.default.unlinkSync(this.file);
|
|
953
|
+
return prior;
|
|
954
|
+
}
|
|
955
|
+
if (!deferred && !prior.deferred && prior.finalizationState === 'complete' && snapshot.some(item => item.outcome === 'pending'))
|
|
956
|
+
throw new Error('cleanup-content-reappeared');
|
|
847
957
|
}
|
|
848
958
|
writeEvidence();
|
|
849
959
|
afterStage?.('evidence');
|
|
@@ -878,9 +988,8 @@ class RunRecord {
|
|
|
878
988
|
result.finalizationState = 'complete';
|
|
879
989
|
writeEvidence();
|
|
880
990
|
afterStage?.('complete-evidence');
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
throw new Error('completion-evidence-conflict');
|
|
991
|
+
if ((0, completion_state_1.canonicalJournal)(this.readCompletion(node_fs_1.default.readFileSync(this.completionFile, 'utf8'))) !== (0, completion_state_1.canonicalJournal)(result))
|
|
992
|
+
throw new Error(`completion-receipt-conflict: ${this.completionFile}: completed receipt changed`);
|
|
884
993
|
// Complete attests this receipt's terminal subset. Only full coverage permits retiring run recovery.
|
|
885
994
|
if (!deferred) {
|
|
886
995
|
finishedInventory();
|
|
@@ -161,7 +161,7 @@ async function runQueueCommand(argv) {
|
|
|
161
161
|
const coordinationFile = node_path_1.default.join(journalDirectory(homes.find(item => item.level === 'Manager')), 'owner-application');
|
|
162
162
|
(0, run_application_1.reserveRunAction)(run, id, revision, expectedSequence, { resolveHome, runDirectories, coordinationFile, homes, readerRoots: () => resolveReaderCorpusRoots(projectPath) });
|
|
163
163
|
const applySequence = run.read().sequence;
|
|
164
|
-
const capability = proposed?.kind === 'capability' ? await (0, run_publication_1.verifyCapabilityDelivery)(proposed, { ...input, operationKey: `${runId}-${id}-${revision}` }, { projectPath, resolveHome, runtimeModule: runtime_1.runtimeModuleStatic, runtimeModulePath: runtime_1.runtimeModulePathStatic }) : undefined;
|
|
164
|
+
const capability = proposed?.kind === 'capability' ? await (0, run_publication_1.verifyCapabilityDelivery)(proposed, { reviewedContent: proposed.application?.reviewedContent, ...input, operationKey: `${runId}-${id}-${revision}` }, { projectPath, resolveHome, runtimeModule: runtime_1.runtimeModuleStatic, runtimeModulePath: runtime_1.runtimeModulePathStatic }) : undefined;
|
|
165
165
|
const capabilities = new Map();
|
|
166
166
|
if (capability)
|
|
167
167
|
capabilities.set(id, capability);
|
|
@@ -169,7 +169,7 @@ async function runQueueCommand(argv) {
|
|
|
169
169
|
const dependency = state.actions[dependencyId];
|
|
170
170
|
if (dependency?.kind !== 'capability')
|
|
171
171
|
continue;
|
|
172
|
-
capabilities.set(dependencyId, await (0, run_publication_1.verifyCapabilityDelivery)(dependency, { publicationRef: dependency.application?.publicationRef, operationKey: `${runId}-${dependencyId}-${dependency.revision}` }, { projectPath, resolveHome, runtimeModule: runtime_1.runtimeModuleStatic, runtimeModulePath: runtime_1.runtimeModulePathStatic }));
|
|
172
|
+
capabilities.set(dependencyId, await (0, run_publication_1.verifyCapabilityDelivery)(dependency, { publicationRef: dependency.application?.publicationRef, reviewedContent: dependency.application?.reviewedContent, operationKey: `${runId}-${dependencyId}-${dependency.revision}` }, { projectPath, resolveHome, runtimeModule: runtime_1.runtimeModuleStatic, runtimeModulePath: runtime_1.runtimeModulePathStatic }));
|
|
173
173
|
}
|
|
174
174
|
return (0, run_application_1.applyRunAction)(run, id, revision, applySequence, {
|
|
175
175
|
resolveHome, runDirectories, coordinationFile, homes, readerRoots: () => resolveReaderCorpusRoots(projectPath),
|
|
@@ -185,7 +185,7 @@ async function runQueueCommand(argv) {
|
|
|
185
185
|
if (node_fs_1.default.existsSync(run.file))
|
|
186
186
|
for (const candidate of Object.values(run.read().actions)) {
|
|
187
187
|
if (candidate.kind === 'capability' && candidate.application?.stage === 'applied')
|
|
188
|
-
capabilities.set(candidate.actionId, await (0, run_publication_1.verifyCapabilityDelivery)(candidate, { publicationRef: candidate.application.publicationRef, operationKey: `${runId}-${candidate.actionId}-${candidate.revision}` }, { projectPath, resolveHome, runtimeModule: runtime_1.runtimeModuleStatic, runtimeModulePath: runtime_1.runtimeModulePathStatic }));
|
|
188
|
+
capabilities.set(candidate.actionId, await (0, run_publication_1.verifyCapabilityDelivery)(candidate, { publicationRef: candidate.application.publicationRef, reviewedContent: candidate.application.reviewedContent, operationKey: `${runId}-${candidate.actionId}-${candidate.revision}` }, { projectPath, resolveHome, runtimeModule: runtime_1.runtimeModuleStatic, runtimeModulePath: runtime_1.runtimeModulePathStatic }));
|
|
189
189
|
}
|
|
190
190
|
return (0, run_application_1.completeRun)(run, input?.summary, homes, { resolveHome, homes, readerRoots: () => resolveReaderCorpusRoots(projectPath), runDirectories: homes.map(journalDirectory), retrospective: input?.retrospective, analyzedSources: input.analyzedSources,
|
|
191
191
|
allowDeferred: input?.allowDeferred === true, deferredSources: input?.deferredSources,
|
|
@@ -232,9 +232,26 @@ function runPackage(plan, cacheDir) {
|
|
|
232
232
|
const plan = resolvePackage();
|
|
233
233
|
const versionCacheDir = path.join(CACHE_ROOT, plan.packageName + '-' + plan.version.replace(/[^a-zA-Z0-9._-]/g, '_'));
|
|
234
234
|
pruneCaches(versionCacheDir, Date.now());
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* True when npx materialized the package into this cache. A non-zero exit from a package
|
|
238
|
+
* that DID install is the CLI's own result, not a cache problem, and must be returned verbatim.
|
|
239
|
+
*/
|
|
240
|
+
function packageMaterialized(cacheDir) {
|
|
241
|
+
try {
|
|
242
|
+
const npxDir = path.join(cacheDir, '_npx');
|
|
243
|
+
return fs.readdirSync(npxDir).some(function (entry) {
|
|
244
|
+
return fs.existsSync(path.join(npxDir, entry, 'node_modules', plan.packageName));
|
|
245
|
+
});
|
|
246
|
+
} catch (error) {
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
235
251
|
let status = runPackage(plan, versionCacheDir);
|
|
236
252
|
|
|
237
|
-
|
|
253
|
+
// Retry ONLY when npx could not materialize the package: the corrupt-cache case this exists for.
|
|
254
|
+
if (status !== 0 && process.platform === 'win32' && !packageMaterialized(versionCacheDir)) {
|
|
238
255
|
const retryCache = path.join(CACHE_ROOT, 'retry-' + Date.now() + '-' + process.pid);
|
|
239
256
|
console.error('[fraim-mcp-launcher] Package execution failed; retrying once with a fresh npm cache.');
|
|
240
257
|
status = runPackage(plan, retryCache);
|
|
@@ -70,6 +70,8 @@ const script_sync_utils_1 = require("../utils/script-sync-utils");
|
|
|
70
70
|
const USER_LEVEL_RUNTIME_DEPS = {
|
|
71
71
|
'node-edge-tts': '*', // used by scripts/author-audio.js
|
|
72
72
|
'adm-zip': '^0.6.0', // used by scripts/communication/*.js (DOCX review layer) — matches root/packages/* manifests
|
|
73
|
+
'fast-glob': '^3.3.3', // used by scripts/exec-with-timeout.ts (Issue #1839)
|
|
74
|
+
'tree-kill': '^1.2.2', // used by scripts/exec-with-timeout.ts (Issue #1839)
|
|
73
75
|
};
|
|
74
76
|
/**
|
|
75
77
|
* Ensure the user-level FRAIM directory structure exists.
|
|
@@ -47,7 +47,6 @@ const REPO_LEARNINGS_REL = (0, project_fraim_paths_1.getWorkspaceFraimDisplayPat
|
|
|
47
47
|
* would keep passing if this default ever changed.
|
|
48
48
|
*/
|
|
49
49
|
exports.DEFAULT_THRESHOLD = 3.0;
|
|
50
|
-
const AGING_HORIZON_DAYS = 7;
|
|
51
50
|
const MAX_ENTRIES_SCANNED = 200;
|
|
52
51
|
const L0_SLEEP_ON_LEARNINGS_PROMPT_MIN = 5;
|
|
53
52
|
// ── Single source of truth for the learning-entry format contract (#533) ──────
|
|
@@ -306,11 +305,10 @@ function getScoreThreshold(workspaceRoot) {
|
|
|
306
305
|
return exports.DEFAULT_THRESHOLD;
|
|
307
306
|
}
|
|
308
307
|
/**
|
|
309
|
-
* Effective score for an L1 learning entry.
|
|
310
|
-
*
|
|
308
|
+
* Effective score for an L1 learning entry. Score is display-only and does
|
|
309
|
+
* not gate delivery; the tier file is the authoritative delivery gate.
|
|
311
310
|
*
|
|
312
|
-
* @param now Optional override for "now"
|
|
313
|
-
* calculations). Defaults to the current wall clock.
|
|
311
|
+
* @param now Optional override for "now". Defaults to the current wall clock.
|
|
314
312
|
*/
|
|
315
313
|
function computeEffectiveScore(severity, lastSeenDate, recurrences, fileType, now = new Date()) {
|
|
316
314
|
const baseScore = severity === 'P-CRITICAL' ? 10 : severity === 'P-HIGH' ? 8 : severity === 'P-MED' ? 5 : 3;
|
|
@@ -330,72 +328,6 @@ function computeEffectiveScore(severity, lastSeenDate, recurrences, fileType, no
|
|
|
330
328
|
const recurrenceBoost = Math.log2(Math.max(1, recurrences) + 1);
|
|
331
329
|
return baseScore * decay * recurrenceBoost;
|
|
332
330
|
}
|
|
333
|
-
function scanMistakePatternFile(filePath, threshold, fileType = 'mistake-patterns') {
|
|
334
|
-
const empty = { active: 0, dormant: 0, agingRisk: 0 };
|
|
335
|
-
if (!(0, fs_1.existsSync)(filePath))
|
|
336
|
-
return empty;
|
|
337
|
-
let content;
|
|
338
|
-
try {
|
|
339
|
-
content = (0, fs_1.readFileSync)(filePath, 'utf8');
|
|
340
|
-
}
|
|
341
|
-
catch {
|
|
342
|
-
return empty;
|
|
343
|
-
}
|
|
344
|
-
const lines = content.split(/\r?\n/);
|
|
345
|
-
const now = new Date();
|
|
346
|
-
const horizon = new Date(now.getTime() + AGING_HORIZON_DAYS * 86_400_000);
|
|
347
|
-
let active = 0;
|
|
348
|
-
let dormant = 0;
|
|
349
|
-
let agingRisk = 0;
|
|
350
|
-
let scanned = 0;
|
|
351
|
-
let inEntry = false;
|
|
352
|
-
let severity = null;
|
|
353
|
-
let lastSeen = '';
|
|
354
|
-
let recurrences = 1;
|
|
355
|
-
const flush = () => {
|
|
356
|
-
if (!severity)
|
|
357
|
-
return;
|
|
358
|
-
scanned++;
|
|
359
|
-
const today = computeEffectiveScore(severity, lastSeen, recurrences, fileType, now);
|
|
360
|
-
if (today >= threshold) {
|
|
361
|
-
active++;
|
|
362
|
-
if (lastSeen) {
|
|
363
|
-
const future = computeEffectiveScore(severity, lastSeen, recurrences, fileType, horizon);
|
|
364
|
-
if (future < threshold)
|
|
365
|
-
agingRisk++;
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
else {
|
|
369
|
-
dormant++;
|
|
370
|
-
}
|
|
371
|
-
};
|
|
372
|
-
for (const line of lines) {
|
|
373
|
-
if (scanned >= MAX_ENTRIES_SCANNED)
|
|
374
|
-
break;
|
|
375
|
-
const headerMatch = line.match((0, learning_entry_format_1.learningEntryHeadingRegex)());
|
|
376
|
-
if (headerMatch) {
|
|
377
|
-
flush();
|
|
378
|
-
inEntry = true;
|
|
379
|
-
severity = headerMatch[1];
|
|
380
|
-
lastSeen = '';
|
|
381
|
-
recurrences = 1;
|
|
382
|
-
continue;
|
|
383
|
-
}
|
|
384
|
-
if (!inEntry)
|
|
385
|
-
continue;
|
|
386
|
-
const lastSeenMatch = line.match(/^\*\*Last seen\*\*:\s*(.+)/);
|
|
387
|
-
if (lastSeenMatch) {
|
|
388
|
-
lastSeen = lastSeenMatch[1].trim();
|
|
389
|
-
continue;
|
|
390
|
-
}
|
|
391
|
-
const recurrenceMatch = line.match(/^\*\*Recurrences\*\*:\s*(\d+)/);
|
|
392
|
-
if (recurrenceMatch) {
|
|
393
|
-
recurrences = parseInt(recurrenceMatch[1], 10);
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
flush();
|
|
397
|
-
return { active, dormant, agingRisk };
|
|
398
|
-
}
|
|
399
331
|
/** Historical counters are only read for the human library view. */
|
|
400
332
|
function resolveUsageStats() {
|
|
401
333
|
try {
|
|
@@ -597,10 +529,10 @@ function mergeLearningEntriesAcrossTiers(tiers) {
|
|
|
597
529
|
return tiers.flatMap(tier => tier.entries);
|
|
598
530
|
}
|
|
599
531
|
const CONTEXT_CATEGORIES = {
|
|
600
|
-
mistake: { ft: 'mistake-patterns',
|
|
601
|
-
pref: { ft: 'preferences',
|
|
602
|
-
coach: { ft: 'manager-coaching',
|
|
603
|
-
validated: { ft: 'validated-patterns',
|
|
532
|
+
mistake: { ft: 'mistake-patterns', label: '(all entries)' },
|
|
533
|
+
pref: { ft: 'preferences', label: '(all entries)' },
|
|
534
|
+
coach: { ft: 'manager-coaching', label: '(manager-facing; all entries)' },
|
|
535
|
+
validated: { ft: 'validated-patterns', label: '(all entries)' },
|
|
604
536
|
};
|
|
605
537
|
/** Resolve the files listed in injected context, in delivery order. */
|
|
606
538
|
function resolveContextLearningFiles(workspaceRoot, resolvedUserId, roots, forJob, domain) {
|
|
@@ -646,7 +578,6 @@ function resolveContextLearningFiles(workspaceRoot, resolvedUserId, roots, forJo
|
|
|
646
578
|
path: tier.path,
|
|
647
579
|
displayPath: tier.displayPath,
|
|
648
580
|
fileType: meta.ft,
|
|
649
|
-
gated: meta.gated,
|
|
650
581
|
label: meta.label,
|
|
651
582
|
isCoach: key === 'coach',
|
|
652
583
|
level: tier.level ?? fallbackLevel,
|
|
@@ -669,42 +600,14 @@ function resolveContextLearningFiles(workspaceRoot, resolvedUserId, roots, forJo
|
|
|
669
600
|
const l1 = collect({ global: l1GlobalCats, domain: l1DomainCats }, (ft, dom) => resolvePersonalLearningFileTiers(roots.repoLearningsBase, roots.managerCacheBase, roots.managerCacheDisplayBase, roots.globalPersonalBase, roots.globalPersonalDisplayBase, dom ? `${resolvedUserId}-${dom}-${ft}.md` : `${resolvedUserId}-${ft}.md`), 'manager');
|
|
670
601
|
return { l2, l1, multiTier, coachPresent };
|
|
671
602
|
}
|
|
672
|
-
/**
|
|
673
|
-
* Report how many entries have been demoted, without naming the file they live in.
|
|
674
|
-
*
|
|
675
|
-
* Issue #861 keeps demoted entries out of the injected context, and #1103 keeps it
|
|
676
|
-
* that way after trying the alternative. Naming the `.cold.md` path here was
|
|
677
|
-
* considered and reverted: the path states a family (`…-mistake-patterns.cold.md`)
|
|
678
|
-
* and not a subject, so an instruction to "read it if this job touches its
|
|
679
|
-
* subject" is one the agent has no basis to follow. It degrades to reading the
|
|
680
|
-
* file every job, which is the token cost #861 removed, or never, which makes the
|
|
681
|
-
* line noise.
|
|
682
|
-
*
|
|
683
|
-
* Recovery for a demoted entry is a synthesis-time concern, not a runtime one.
|
|
684
|
-
* `sleep-on-learnings` reads both tiers when deciding whether a signal is a
|
|
685
|
-
* recurrence, so an entry that becomes relevant again is found, updated and
|
|
686
|
-
* promoted there. See that job's gather-signals and analyze phases.
|
|
687
|
-
*/
|
|
688
|
-
function renderDemotedLine(listed, label) {
|
|
689
|
-
const total = listed.reduce((sum, f) => sum + f.dormant, 0);
|
|
690
|
-
if (total === 0)
|
|
691
|
-
return '';
|
|
692
|
-
return total === 1
|
|
693
|
-
? `Demoted: 1 ${label} pattern has gone quiet and is not listed above.\n`
|
|
694
|
-
: `Demoted: ${total} ${label} patterns have gone quiet and are not listed above.\n`;
|
|
695
|
-
}
|
|
696
603
|
function buildLearningContextSection(workspaceRoot, userId, forJob, domain) {
|
|
697
604
|
const roots = getLearningRoots(workspaceRoot);
|
|
698
605
|
const resolvedUserId = resolveLearningUserId(workspaceRoot, userId, roots);
|
|
699
|
-
const threshold = getScoreThreshold(workspaceRoot);
|
|
700
606
|
const resolved = resolveContextLearningFiles(workspaceRoot, resolvedUserId, roots, forJob, domain);
|
|
701
607
|
const multiTier = resolved.multiTier;
|
|
702
608
|
const toListed = (f) => ({
|
|
703
609
|
displayPath: f.displayPath,
|
|
704
610
|
label: f.label,
|
|
705
|
-
dormant: f.gated
|
|
706
|
-
? scanMistakePatternFile(f.path, threshold, f.fileType).dormant
|
|
707
|
-
: 0,
|
|
708
611
|
isCoach: f.isCoach,
|
|
709
612
|
});
|
|
710
613
|
const l2Files = resolved.l2.map(toListed);
|
|
@@ -726,14 +629,12 @@ function buildLearningContextSection(workspaceRoot, userId, forJob, domain) {
|
|
|
726
629
|
section += '### L2 - Org patterns\n';
|
|
727
630
|
for (const f of l2Files)
|
|
728
631
|
section += `\`${f.displayPath}\` ${f.label}\n`;
|
|
729
|
-
section += renderDemotedLine(l2Files, 'org');
|
|
730
632
|
section += '\n';
|
|
731
633
|
}
|
|
732
634
|
if (hasL1) {
|
|
733
635
|
section += '### L1 - Your patterns\n';
|
|
734
636
|
for (const f of l1Files)
|
|
735
637
|
section += `\`${f.displayPath}\` ${f.label}\n`;
|
|
736
|
-
section += renderDemotedLine(l1Files, 'personal');
|
|
737
638
|
section += '\n';
|
|
738
639
|
}
|
|
739
640
|
if (l0CoachingCount > 0 || l0RetroCount > 0) {
|
|
@@ -1409,13 +1310,11 @@ function listDomainLearningFiles(dir, prefix, fileType) {
|
|
|
1409
1310
|
}
|
|
1410
1311
|
// Parse the `## [P-…] Title` entries (and their body prose) out of one learning
|
|
1411
1312
|
// file. Score metadata (Last seen / Recurrences) is captured to compute the
|
|
1412
|
-
// effective decayed score
|
|
1413
|
-
//
|
|
1313
|
+
// effective decayed score for display; those bookkeeping lines are excluded from
|
|
1314
|
+
// the rendered body.
|
|
1414
1315
|
//
|
|
1415
|
-
//
|
|
1416
|
-
|
|
1417
|
-
// all entries are injected.
|
|
1418
|
-
function parseLearningEntries(filePath, displayPath, category, level, domain = 'global', tier, threshold = exports.DEFAULT_THRESHOLD, usageStats) {
|
|
1316
|
+
// `injected` reflects tier standing (hot = true, cold = false); score is display-only.
|
|
1317
|
+
function parseLearningEntries(filePath, displayPath, category, level, domain = 'global', tier, usageStats) {
|
|
1419
1318
|
if (!(0, fs_1.existsSync)(filePath))
|
|
1420
1319
|
return [];
|
|
1421
1320
|
let content;
|
|
@@ -1426,7 +1325,7 @@ function parseLearningEntries(filePath, displayPath, category, level, domain = '
|
|
|
1426
1325
|
return [];
|
|
1427
1326
|
}
|
|
1428
1327
|
const fileType = exports.CATEGORY_TO_FILETYPE[category];
|
|
1429
|
-
//
|
|
1328
|
+
// mistake-patterns and validated-patterns carry a display score; other categories do not.
|
|
1430
1329
|
const isScoreGated = fileType === 'mistake-patterns' || fileType === 'validated-patterns';
|
|
1431
1330
|
const out = [];
|
|
1432
1331
|
let current = null;
|
|
@@ -1443,12 +1342,11 @@ function parseLearningEntries(filePath, displayPath, category, level, domain = '
|
|
|
1443
1342
|
if (curLastSeen !== null) {
|
|
1444
1343
|
const s = computeEffectiveScore(current.severity, curLastSeen, curRecurrences, fileType, new Date());
|
|
1445
1344
|
current.score = s;
|
|
1446
|
-
current.injected =
|
|
1345
|
+
current.injected = tier !== 'cold';
|
|
1447
1346
|
}
|
|
1448
1347
|
else {
|
|
1449
1348
|
current.score = null;
|
|
1450
|
-
|
|
1451
|
-
current.injected = true;
|
|
1349
|
+
current.injected = tier !== 'cold';
|
|
1452
1350
|
}
|
|
1453
1351
|
}
|
|
1454
1352
|
else {
|
|
@@ -1620,7 +1518,6 @@ function readRejectedProposals(workspaceRoot, userId) {
|
|
|
1620
1518
|
}
|
|
1621
1519
|
function readPreservedLearnings(workspaceRoot, userId, scope, level = 'machine') {
|
|
1622
1520
|
const roots = getLearningRoots(workspaceRoot);
|
|
1623
|
-
const threshold = getScoreThreshold(workspaceRoot);
|
|
1624
1521
|
const usageStats = resolveUsageStats();
|
|
1625
1522
|
// Only the human library reads these observations. An unavailable store must
|
|
1626
1523
|
// fail this read, so a client can retain known history rather than invent zeroes.
|
|
@@ -1657,7 +1554,7 @@ function readPreservedLearnings(workspaceRoot, userId, scope, level = 'machine')
|
|
|
1657
1554
|
// ResolvedLearningTier uses 'manager' to mean machine-level personal; map it to 'machine'.
|
|
1658
1555
|
const tierLevel = (t) => t.level === 'manager' ? 'machine' : t.level;
|
|
1659
1556
|
const parseTiers = (tiers, cat, tierVal, domainVal) => mergeLearningEntriesAcrossTiers(tiers.map((t) => ({
|
|
1660
|
-
entries: parseLearningEntries(t.path, t.displayPath, cat, tierLevel(t), domainVal ?? 'global', tierVal,
|
|
1557
|
+
entries: parseLearningEntries(t.path, t.displayPath, cat, tierLevel(t), domainVal ?? 'global', tierVal, usageStats)
|
|
1661
1558
|
.map(entry => decorateAssessment(entry, t.path)),
|
|
1662
1559
|
})));
|
|
1663
1560
|
if (scope === 'org') {
|
|
@@ -1679,7 +1576,7 @@ function readPreservedLearnings(workspaceRoot, userId, scope, level = 'machine')
|
|
|
1679
1576
|
seenDomainFile.add(fileName);
|
|
1680
1577
|
const domainTiers = resolveOrgLearningFileTiers(roots.repoLearningsBase, fileName);
|
|
1681
1578
|
if (domainTiers.length === 0) {
|
|
1682
|
-
out.push(...parseLearningEntries((0, path_1.join)(base, fileName), `${REPO_LEARNINGS_REL}/${fileName}`, cat, 'org', domain, undefined,
|
|
1579
|
+
out.push(...parseLearningEntries((0, path_1.join)(base, fileName), `${REPO_LEARNINGS_REL}/${fileName}`, cat, 'org', domain, undefined, usageStats)
|
|
1683
1580
|
.map(entry => decorateAssessment(entry, (0, path_1.join)(base, fileName))));
|
|
1684
1581
|
}
|
|
1685
1582
|
else {
|