fraim-hub 2.0.339 → 2.0.341
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/completion-state.js +161 -0
- package/dist/src/ai-hub/desktop-main.js +50 -0
- package/dist/src/ai-hub/hosts.js +46 -0
- package/dist/src/ai-hub/manager-turns.js +16 -0
- package/dist/src/ai-hub/server.js +215 -75
- package/dist/src/core/handoff-contracts.js +718 -0
- package/dist/src/core/proposal-set-contract.js +214 -0
- package/dist/src/core/run-recovery-state.js +9 -0
- package/dist/src/local-mcp-server/artifact-retention-cleanup.js +36 -9
- package/dist/src/local-mcp-server/learning-context-builder.js +224 -20
- package/dist/src/local-mcp-server/learning-usage-eligibility.js +59 -0
- package/dist/src/local-mcp-server/learning-usage-events.js +63 -57
- package/dist/src/local-mcp-server/learning-usage-projection.js +5 -1
- package/dist/src/local-mcp-server/learning-usage-store.js +235 -79
- package/package.json +7 -2
- package/public/ai-hub/coach-intake-surface.js +415 -0
- package/public/ai-hub/index.html +21 -63
- package/public/ai-hub/pane-surfaces.js +315 -0
- package/public/ai-hub/review.css +420 -0
- package/public/ai-hub/script.js +1211 -378
- package/public/ai-hub/styles.css +114 -40
|
@@ -37,6 +37,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
exports.findAvailablePortExcluding = exports.findAvailablePort = exports.AiHubServer = exports.HubConnectorStatusStore = exports.HostConfigStore = exports.DeploymentStore = void 0;
|
|
40
|
+
exports.normalizeProposalDecisions = normalizeProposalDecisions;
|
|
41
|
+
exports.applyProposalSetToRun = applyProposalSetToRun;
|
|
40
42
|
exports.selfHealBrokenManagedAgents = selfHealBrokenManagedAgents;
|
|
41
43
|
exports.configureFraimForHubAgent = configureFraimForHubAgent;
|
|
42
44
|
exports.installAgentAndRefreshDetection = installAgentAndRefreshDetection;
|
|
@@ -45,6 +47,9 @@ exports.__setCatalogSyncSpawnForTests = __setCatalogSyncSpawnForTests;
|
|
|
45
47
|
exports.__setAgentInstallTimeoutMsForTests = __setAgentInstallTimeoutMsForTests;
|
|
46
48
|
exports.hubRunProcess = hubRunProcess;
|
|
47
49
|
exports.buildOpenFileInvocation = buildOpenFileInvocation;
|
|
50
|
+
exports.buildHubRecoveryContinueMessage = buildHubRecoveryContinueMessage;
|
|
51
|
+
exports.buildHubCompactionRecoveryContinueMessage = buildHubCompactionRecoveryContinueMessage;
|
|
52
|
+
const handoff_contracts_1 = require("../core/handoff-contracts");
|
|
48
53
|
const express_1 = __importDefault(require("express"));
|
|
49
54
|
const path_1 = __importDefault(require("path"));
|
|
50
55
|
const fs_1 = __importDefault(require("fs"));
|
|
@@ -53,8 +58,10 @@ const crypto_1 = require("crypto");
|
|
|
53
58
|
const child_process_1 = require("child_process");
|
|
54
59
|
const https_1 = __importDefault(require("https"));
|
|
55
60
|
const native_voice_input_1 = require("./native-voice-input");
|
|
61
|
+
const proposal_set_contract_1 = require("../core/proposal-set-contract");
|
|
56
62
|
const types_1 = require("../first-run/types");
|
|
57
63
|
const local_provider_registry_1 = require("../cli/providers/local-provider-registry");
|
|
64
|
+
const learning_domains_1 = require("../config/learning-domains");
|
|
58
65
|
const learning_context_builder_1 = require("../local-mcp-server/learning-context-builder");
|
|
59
66
|
const brand_store_1 = require("../core/brand-store");
|
|
60
67
|
const project_fraim_paths_1 = require("../core/utils/project-fraim-paths");
|
|
@@ -976,12 +983,64 @@ function normalizeDeliveryActionResult(raw) {
|
|
|
976
983
|
...(typeof value.domainActionCompleted === 'boolean' ? { domainActionCompleted: value.domainActionCompleted } : {}),
|
|
977
984
|
};
|
|
978
985
|
}
|
|
986
|
+
/**
|
|
987
|
+
* Issue #1857. The proxy already rejects a malformed set and tells the agent which
|
|
988
|
+
* field to fix, so this is the second gate rather than the first: it protects the
|
|
989
|
+
* render from a payload that reached the Hub by some other path. Same fail-closed
|
|
990
|
+
* rule as normalizeReviewHandoff, because the failure this design exists to remove
|
|
991
|
+
* was a card rendering with its body missing rather than throwing.
|
|
992
|
+
*/
|
|
993
|
+
/**
|
|
994
|
+
* Issue #1857 R18. A relayed decision is only useful if it names the proposal it
|
|
995
|
+
* decided; a title cannot, because `move-artifact-between-levels` states that
|
|
996
|
+
* titles never select updates. Anything without a usable proposalId and a known
|
|
997
|
+
* action is dropped here rather than forwarded, so the job never records a
|
|
998
|
+
* decision against a proposal the manager did not actually resolve.
|
|
999
|
+
*/
|
|
1000
|
+
function normalizeProposalDecisions(raw) {
|
|
1001
|
+
if (!Array.isArray(raw))
|
|
1002
|
+
return [];
|
|
1003
|
+
const out = [];
|
|
1004
|
+
for (const entry of raw) {
|
|
1005
|
+
if (!entry || typeof entry !== 'object')
|
|
1006
|
+
continue;
|
|
1007
|
+
const value = entry;
|
|
1008
|
+
const proposalId = typeof value.proposalId === 'string' ? value.proposalId.trim() : '';
|
|
1009
|
+
const action = value.action === 'approve' || value.action === 'reject' ? value.action : null;
|
|
1010
|
+
if (!proposalId || !action)
|
|
1011
|
+
continue;
|
|
1012
|
+
out.push({
|
|
1013
|
+
proposalId,
|
|
1014
|
+
action,
|
|
1015
|
+
...(Number.isInteger(value.revision) ? { revision: value.revision } : {}),
|
|
1016
|
+
...(typeof value.level === 'string' && value.level.trim() ? { level: value.level.trim() } : {}),
|
|
1017
|
+
...(typeof value.reason === 'string' && value.reason.trim() ? { reason: value.reason.trim() } : {}),
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
return out;
|
|
1021
|
+
}
|
|
1022
|
+
function normalizeProposalSet(raw) {
|
|
1023
|
+
if ((0, proposal_set_contract_1.validateProposalSet)(raw))
|
|
1024
|
+
return null;
|
|
1025
|
+
if (!raw || typeof raw !== 'object')
|
|
1026
|
+
return null;
|
|
1027
|
+
const value = raw;
|
|
1028
|
+
return {
|
|
1029
|
+
runScope: value.runScope,
|
|
1030
|
+
// Issue #1857 R3: strongest evidence first, decided here rather than trusted
|
|
1031
|
+
// from the agent, so the order the manager reads does not depend on how a
|
|
1032
|
+
// model happened to sort its output.
|
|
1033
|
+
proposals: (0, proposal_set_contract_1.rankProposals)(value.proposals),
|
|
1034
|
+
...(value.suppressed?.length ? { suppressed: value.suppressed } : {}),
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
979
1037
|
function normalizeReviewHandoff(raw) {
|
|
980
1038
|
if (!raw || typeof raw !== 'object')
|
|
981
1039
|
return null;
|
|
982
1040
|
const value = raw;
|
|
983
1041
|
if (typeof value.reviewRequired !== 'boolean')
|
|
984
1042
|
return null;
|
|
1043
|
+
const producingJobName = typeof value.producingJobName === 'string' ? value.producingJobName.trim() : '';
|
|
985
1044
|
const artifacts = Array.isArray(value.artifacts)
|
|
986
1045
|
? value.artifacts.map((artifact, index) => normalizeReviewArtifact(artifact, index)).filter((artifact) => Boolean(artifact))
|
|
987
1046
|
: [];
|
|
@@ -995,6 +1054,7 @@ function normalizeReviewHandoff(raw) {
|
|
|
995
1054
|
: '';
|
|
996
1055
|
if (!value.reviewRequired) {
|
|
997
1056
|
return {
|
|
1057
|
+
...(producingJobName ? { producingJobName } : {}),
|
|
998
1058
|
reviewRequired: false,
|
|
999
1059
|
reviewTarget: rawTarget ? { type: targetType || 'none', label: targetLabel || 'Completed work' } : null,
|
|
1000
1060
|
artifacts,
|
|
@@ -1010,6 +1070,7 @@ function normalizeReviewHandoff(raw) {
|
|
|
1010
1070
|
if (artifacts.length > 0)
|
|
1011
1071
|
return null;
|
|
1012
1072
|
return {
|
|
1073
|
+
...(producingJobName ? { producingJobName } : {}),
|
|
1013
1074
|
reviewRequired: true,
|
|
1014
1075
|
reviewTarget: { type: 'pull_request', label: targetLabel || 'Pull request', url },
|
|
1015
1076
|
artifacts: [],
|
|
@@ -1027,6 +1088,7 @@ function normalizeReviewHandoff(raw) {
|
|
|
1027
1088
|
const fileArtifacts = artifacts.filter((artifact) => artifact.path && !artifact.url);
|
|
1028
1089
|
if (targetType === 'artifact_set' && fileArtifacts.length === artifacts.length) {
|
|
1029
1090
|
return {
|
|
1091
|
+
...(producingJobName ? { producingJobName } : {}),
|
|
1030
1092
|
reviewRequired: true,
|
|
1031
1093
|
reviewTarget: { type: 'artifact_set', label: targetLabel || `${artifacts.length} artifact${artifacts.length === 1 ? '' : 's'}` },
|
|
1032
1094
|
artifacts: fileArtifacts,
|
|
@@ -1392,6 +1454,7 @@ function buildAgentSwitchHandoffSummary(conversation) {
|
|
|
1392
1454
|
recentEvents: events,
|
|
1393
1455
|
artifacts,
|
|
1394
1456
|
reviewHandoff: conversation.reviewHandoff || null,
|
|
1457
|
+
proposalSet: conversation.proposalSet || null,
|
|
1395
1458
|
pendingDeliveryActionId: conversation.pendingDeliveryActionId ?? null,
|
|
1396
1459
|
selectedReviewAction: normalizeReviewAction(conversation.selectedReviewAction) || null,
|
|
1397
1460
|
completedDeliveryActionId: conversation.completedDeliveryActionId ?? null,
|
|
@@ -1615,31 +1678,35 @@ function isCodexMissingReasoningResumeText(value) {
|
|
|
1615
1678
|
return text.includes("provided without its required 'reasoning' item") ||
|
|
1616
1679
|
text.includes('provided without its required "reasoning" item');
|
|
1617
1680
|
}
|
|
1618
|
-
//
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
//
|
|
1638
|
-
//
|
|
1639
|
-
//
|
|
1640
|
-
//
|
|
1641
|
-
|
|
1642
|
-
|
|
1681
|
+
// Review ownership follows the submitted deliverable, independently of tracker identity.
|
|
1682
|
+
/**
|
|
1683
|
+
* Issue #1857. Mirrors applyReviewHandoffToRun's rule: an unnormalized payload
|
|
1684
|
+
* leaves prior run state untouched rather than replacing a good batch with a bad
|
|
1685
|
+
* one. Does not set pauseReason; that transition belongs to classifyExit at turn
|
|
1686
|
+
* exit, for the same reason it does for reviewHandoff.
|
|
1687
|
+
*/
|
|
1688
|
+
function applyProposalSetToRun(run, proposalSet, onFailure) {
|
|
1689
|
+
const normalized = normalizeProposalSet(proposalSet);
|
|
1690
|
+
if (!normalized) {
|
|
1691
|
+
// Issue #1857. Prior state is left untouched, matching applyReviewHandoffToRun:
|
|
1692
|
+
// a batch the Hub cannot trust must not replace one it can.
|
|
1693
|
+
//
|
|
1694
|
+
// But silence here would be the very failure this issue exists to remove. The
|
|
1695
|
+
// proxy contract rejects a malformed set before it ever gets this far and tells
|
|
1696
|
+
// the agent which field to fix, so reaching this line means the payload arrived
|
|
1697
|
+
// by some path that check does not cover: an older client, a replay, a direct
|
|
1698
|
+
// post. In that case the manager would otherwise open an empty review surface
|
|
1699
|
+
// with nothing anywhere saying a batch was dropped, which is exactly how the
|
|
1700
|
+
// `## Pending Review` parser failed for a year.
|
|
1701
|
+
//
|
|
1702
|
+
// Report it on the same channel a rejected reviewHandoff uses (#1900), naming
|
|
1703
|
+
// the fields, so the run carries a recoverable error the agent can act on.
|
|
1704
|
+
const errors = (0, proposal_set_contract_1.validateProposalSet)(proposalSet) || ['the payload could not be normalized'];
|
|
1705
|
+
onFailure(`the learning review batch was not applied: ${errors.join('; ')}`);
|
|
1706
|
+
return;
|
|
1707
|
+
}
|
|
1708
|
+
run.proposalSet = normalized;
|
|
1709
|
+
}
|
|
1643
1710
|
function applyReviewHandoffToRun(run, reviewHandoff) {
|
|
1644
1711
|
const normalizedReviewHandoff = normalizeReviewHandoff(reviewHandoff);
|
|
1645
1712
|
if (!normalizedReviewHandoff) {
|
|
@@ -1663,7 +1730,7 @@ function applyReviewHandoffToRun(run, reviewHandoff) {
|
|
|
1663
1730
|
}
|
|
1664
1731
|
// Issue #1447: a mismatched `pull_request` target, isolated for readability.
|
|
1665
1732
|
function pullRequestTargetMismatch(stored, submittedTarget) {
|
|
1666
|
-
const submittedUrl =
|
|
1733
|
+
const submittedUrl = (0, url_safety_1.safeHttpUrl)(submittedTarget?.url) || undefined;
|
|
1667
1734
|
if (stored.reviewTarget?.type !== 'pull_request' || stored.reviewTarget.url !== submittedUrl) {
|
|
1668
1735
|
return `stored reviewTarget (${JSON.stringify(stored.reviewTarget)}) does not match the submitted pull_request target (url: "${submittedUrl}")`;
|
|
1669
1736
|
}
|
|
@@ -1680,7 +1747,7 @@ function artifactSetMismatch(stored, submitted) {
|
|
|
1680
1747
|
const rawArtifacts = submitted.artifacts;
|
|
1681
1748
|
const rawArtifactList = Array.isArray(rawArtifacts) ? rawArtifacts : [];
|
|
1682
1749
|
const submittedPaths = rawArtifactList
|
|
1683
|
-
.map((artifact) => (artifact && typeof artifact.path === 'string' ? artifact.path : null))
|
|
1750
|
+
.map((artifact) => (artifact && typeof artifact.path === 'string' ? artifact.path.trim() : null))
|
|
1684
1751
|
.filter((path) => Boolean(path))
|
|
1685
1752
|
.sort();
|
|
1686
1753
|
const storedPaths = (stored.artifacts || [])
|
|
@@ -1693,8 +1760,7 @@ function artifactSetMismatch(stored, submitted) {
|
|
|
1693
1760
|
return null;
|
|
1694
1761
|
}
|
|
1695
1762
|
// Issue #1447: one generic post-condition instead of enumerating each way a
|
|
1696
|
-
// review handoff can fail to land.
|
|
1697
|
-
// filtering, normalization rejecting the whole payload, or a per-artifact
|
|
1763
|
+
// review handoff can fail to land. Whether normalization rejects the whole payload or a per-artifact
|
|
1698
1764
|
// normalization failure silently dropping one item while the handoff as a
|
|
1699
1765
|
// whole still normalizes — this compares what was actually submitted against
|
|
1700
1766
|
// what the run now holds and fails loudly on any structural mismatch. Only
|
|
@@ -1702,33 +1768,29 @@ function artifactSetMismatch(stored, submitted) {
|
|
|
1702
1768
|
// issue's incident describes (a stale or absent handoff masquerading as a
|
|
1703
1769
|
// successful submission); a `reviewRequired: false` payload carries no such
|
|
1704
1770
|
// promise.
|
|
1705
|
-
function verifyReviewHandoffReflectsSubmission(run, submitted,
|
|
1771
|
+
function verifyReviewHandoffReflectsSubmission(run, submitted, onFailure) {
|
|
1706
1772
|
if (!submitted || typeof submitted !== 'object' || submitted.reviewRequired !== true)
|
|
1707
1773
|
return;
|
|
1708
|
-
if (crossJobName) {
|
|
1709
|
-
failLoudlyOnDroppedReviewHandoff(run, `a review handoff was submitted under job "${crossJobName}", but this run is tracking job "${run.jobId}" — the cross-job guard did not apply it`);
|
|
1710
|
-
return;
|
|
1711
|
-
}
|
|
1712
1774
|
const stored = run.reviewHandoff;
|
|
1713
1775
|
const submittedTarget = submitted.reviewTarget;
|
|
1714
1776
|
if (!stored || stored.reviewRequired !== true) {
|
|
1715
|
-
|
|
1777
|
+
onFailure(`evidence.reviewHandoff failed validation and was rejected (submitted reviewTarget: ${JSON.stringify(submittedTarget) || 'none'})`);
|
|
1716
1778
|
return;
|
|
1717
1779
|
}
|
|
1718
|
-
const submittedType = submittedTarget && typeof submittedTarget.type === 'string' ? submittedTarget.type : undefined;
|
|
1780
|
+
const submittedType = submittedTarget && typeof submittedTarget.type === 'string' ? submittedTarget.type.trim() : undefined;
|
|
1719
1781
|
const mismatch = submittedType === 'pull_request'
|
|
1720
1782
|
? pullRequestTargetMismatch(stored, submittedTarget)
|
|
1721
1783
|
: artifactSetMismatch(stored, submitted);
|
|
1722
1784
|
if (mismatch)
|
|
1723
|
-
|
|
1785
|
+
onFailure(mismatch);
|
|
1724
1786
|
}
|
|
1725
1787
|
// Apply a parsed seekMentoring tool-use signal from the host stream to
|
|
1726
1788
|
// the run state. Returns the updated currentPhase.
|
|
1727
|
-
function applySeekMentoringSignal(run, signal) {
|
|
1789
|
+
function applySeekMentoringSignal(run, signal, onReviewFailure) {
|
|
1728
1790
|
// Issue #347: filter cross-job pollution. The agent may call
|
|
1729
1791
|
// seekMentoring for jobs OTHER than the one this run is tracking
|
|
1730
1792
|
// (e.g., consulting `organizational-learning-synthesis` mid-run).
|
|
1731
|
-
// Only
|
|
1793
|
+
// Only tracker metadata requires matching job identity.
|
|
1732
1794
|
// Issue #732: match on `jobName` — the stable job-name slug that equals
|
|
1733
1795
|
// run.jobId — NOT `jobId`. get_fraim_job mints a fresh randomUUID as the
|
|
1734
1796
|
// job id on every call and the agent echoes that UUID into
|
|
@@ -1739,33 +1801,30 @@ function applySeekMentoringSignal(run, signal) {
|
|
|
1739
1801
|
const targetJobId = run.jobId;
|
|
1740
1802
|
const callJobName = signal.jobName;
|
|
1741
1803
|
const isCrossJob = Boolean(callJobName && targetJobId && callJobName !== targetJobId);
|
|
1742
|
-
// Issue #
|
|
1743
|
-
//
|
|
1744
|
-
//
|
|
1745
|
-
//
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
if (signal.
|
|
1749
|
-
|
|
1750
|
-
applyReviewHandoffToRun(run, signal.reviewHandoff);
|
|
1751
|
-
verifyReviewHandoffReflectsSubmission(run, signal.reviewHandoff, isCrossJob ? callJobName : null);
|
|
1752
|
-
}
|
|
1753
|
-
else if (!isCrossJob) {
|
|
1754
|
-
// Issue #1501: No reviewHandoff in this same-job call. Clear any prior reviewHandoff so
|
|
1755
|
-
// run.reviewHandoff always reflects the most recent seekMentoring intent,
|
|
1756
|
-
// not a stale flag from an earlier submit phase.
|
|
1757
|
-
run.reviewHandoff = null;
|
|
1758
|
-
}
|
|
1759
|
-
if (isCrossJob)
|
|
1804
|
+
// Issue #1857: the learning review batch, applied on the same terms as the
|
|
1805
|
+
// handoff above it. Placed before the early returns, because a review-pending
|
|
1806
|
+
// call that also reports a handoff failure still emitted a real batch, and a
|
|
1807
|
+
// dropped set is invisible: the surface renders nothing and says nothing.
|
|
1808
|
+
if (signal.proposalSet)
|
|
1809
|
+
applyProposalSetToRun(run, signal.proposalSet, onReviewFailure);
|
|
1810
|
+
if (signal.reviewHandoffError) {
|
|
1811
|
+
onReviewFailure(signal.reviewHandoffError);
|
|
1760
1812
|
return;
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1813
|
+
}
|
|
1814
|
+
else if (signal.reviewHandoff) {
|
|
1815
|
+
const error = (0, handoff_contracts_1.reviewHandoffNormalizationError)(signal.reviewHandoff);
|
|
1816
|
+
if (error) {
|
|
1817
|
+
onReviewFailure(error);
|
|
1818
|
+
return;
|
|
1819
|
+
}
|
|
1820
|
+
else {
|
|
1821
|
+
applyReviewHandoffToRun(run, { ...signal.reviewHandoff, producingJobName: callJobName });
|
|
1822
|
+
verifyReviewHandoffReflectsSubmission(run, signal.reviewHandoff, onReviewFailure);
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
else {
|
|
1826
|
+
// The most recent mentoring intent owns review lifetime across all jobs.
|
|
1827
|
+
run.reviewHandoff = null;
|
|
1769
1828
|
}
|
|
1770
1829
|
if (signal.deliveryActionId !== undefined) {
|
|
1771
1830
|
run.pendingDeliveryActionId = signal.deliveryActionId;
|
|
@@ -1785,6 +1844,17 @@ function applySeekMentoringSignal(run, signal) {
|
|
|
1785
1844
|
run.pendingDeliveryActionId = null;
|
|
1786
1845
|
}
|
|
1787
1846
|
}
|
|
1847
|
+
// Delivery actions belong to the review, while tracker and issue metadata
|
|
1848
|
+
// remain scoped to the run's original job (including colliding phase names).
|
|
1849
|
+
if (isCrossJob)
|
|
1850
|
+
return;
|
|
1851
|
+
if (signal.issueNumber)
|
|
1852
|
+
run.issueNumber = signal.issueNumber;
|
|
1853
|
+
if (signal.nextJobRecommendations) {
|
|
1854
|
+
const normalized = normalizeNextJobRecommendations(signal.nextJobRecommendations);
|
|
1855
|
+
if (normalized)
|
|
1856
|
+
run.nextJobRecommendations = normalized;
|
|
1857
|
+
}
|
|
1788
1858
|
if (signal.delegationLedger &&
|
|
1789
1859
|
run.jobId === 'fully-delegate' &&
|
|
1790
1860
|
signal.phaseStatus === 'complete' &&
|
|
@@ -2468,10 +2538,11 @@ function buildHubRecoveryContinueMessage(run, exitCode, attempt) {
|
|
|
2468
2538
|
'The FRAIM Hub is automatically recovering this interrupted run.',
|
|
2469
2539
|
'This is not a manager-authored instruction. Do not say the manager asked you to continue.',
|
|
2470
2540
|
`Run id: ${run.id}`,
|
|
2471
|
-
`
|
|
2541
|
+
`CLI host session id: ${run.sessionId || 'unknown'}`,
|
|
2472
2542
|
`Recovery attempt: ${attempt}/${MAX_RECOVERY_ATTEMPTS}`,
|
|
2473
2543
|
`Prior exit code: ${exitCode ?? 'unknown'}`,
|
|
2474
2544
|
'Resume only if the tracked FRAIM phase is non-terminal and not waiting for human review or approval.',
|
|
2545
|
+
(0, manager_turns_1.buildHubPhaseRecoveryInstructions)(run.id),
|
|
2475
2546
|
].join('\n');
|
|
2476
2547
|
}
|
|
2477
2548
|
// Issue #1221: the terminal exit/recovery system events previously said only
|
|
@@ -2515,10 +2586,11 @@ function buildHubCompactionRecoveryContinueMessage(run, exitCode, attempt) {
|
|
|
2515
2586
|
'The host compacted context or restarted its internal runtime while this run was still active.',
|
|
2516
2587
|
'This is not a manager-authored instruction. Do not say the manager asked you to continue.',
|
|
2517
2588
|
`Run id: ${run.id}`,
|
|
2518
|
-
`
|
|
2589
|
+
`CLI host session id: ${run.sessionId || 'unknown'}`,
|
|
2519
2590
|
`Compaction recovery attempt: ${attempt}/${MAX_RECOVERY_ATTEMPTS}`,
|
|
2520
2591
|
`Prior exit code: ${exitCode ?? 'unknown'}`,
|
|
2521
2592
|
'Resume only if the tracked FRAIM phase is non-terminal and not waiting for human review or approval.',
|
|
2593
|
+
(0, manager_turns_1.buildHubPhaseRecoveryInstructions)(run.id),
|
|
2522
2594
|
].join('\n');
|
|
2523
2595
|
}
|
|
2524
2596
|
function createHubCompactionRecoveryEvent(run, exitCode, attempt) {
|
|
@@ -3515,6 +3587,10 @@ class AiHubServer {
|
|
|
3515
3587
|
project.droppedRecordedReason = this.droppedRecordedReason;
|
|
3516
3588
|
}
|
|
3517
3589
|
const rawJobs = (0, catalog_1.discoverEmployeeJobs)(normalizedProjectPath, FULL_JOB_CATALOG_OPTIONS);
|
|
3590
|
+
// Issue #1891: build the custom-employee index once (O(1) per job, one disk walk total)
|
|
3591
|
+
// so custom ownership wins over catalog assignment. Follows the same pattern as
|
|
3592
|
+
// buildCustomJobOwnerIndex documented in the Issue #1090 comment above.
|
|
3593
|
+
const customJobOwners = buildCustomJobOwnerIndex(normalizedProjectPath ?? '');
|
|
3518
3594
|
// Issue #566 (R7): jobs already carry `personalized` from catalog discovery
|
|
3519
3595
|
// (true for the fraim/personalized-employee layer). The Hub renders a plain
|
|
3520
3596
|
// "Personalized" marking from that flag — no author/attribution is tracked.
|
|
@@ -3522,7 +3598,7 @@ class AiHubServer {
|
|
|
3522
3598
|
.filter((job) => !FRAIM_INTERNAL_JOB_IDS.has(job.id))
|
|
3523
3599
|
.map((job) => ({
|
|
3524
3600
|
...job,
|
|
3525
|
-
requiredPersonaKey: getProtectedPersonaForHubJob(job.id),
|
|
3601
|
+
requiredPersonaKey: customJobOwners.get(job.id) ?? getProtectedPersonaForHubJob(job.id),
|
|
3526
3602
|
}));
|
|
3527
3603
|
// Issue #1247: expose internal jobs in a separate bootstrap field so next-job
|
|
3528
3604
|
// recommendation chips can resolve and launch them. They are intentionally
|
|
@@ -3711,6 +3787,9 @@ class AiHubServer {
|
|
|
3711
3787
|
env: {
|
|
3712
3788
|
...(launchContext.env || {}),
|
|
3713
3789
|
FRAIM_HUB_RUN_ID: runId,
|
|
3790
|
+
// Each host turn is a new phase-arrival scope; MCP restarts within
|
|
3791
|
+
// that host inherit the same token and retain their valid starts.
|
|
3792
|
+
FRAIM_PHASE_RESUME_ID: (0, crypto_1.randomUUID)(),
|
|
3714
3793
|
FRAIM_BROWSER_CONVERSATION_ID: conversationId,
|
|
3715
3794
|
...(page ? {
|
|
3716
3795
|
FRAIM_BROWSER_PAGE_TARGET_ID: page.targetId,
|
|
@@ -4059,6 +4138,7 @@ class AiHubServer {
|
|
|
4059
4138
|
eventLogRefs: run.eventLogRefs || [],
|
|
4060
4139
|
artifacts: run.artifacts || [],
|
|
4061
4140
|
reviewHandoff: run.reviewHandoff || null,
|
|
4141
|
+
proposalSet: run.proposalSet || null,
|
|
4062
4142
|
pendingDeliveryActionId: run.pendingDeliveryActionId ?? null,
|
|
4063
4143
|
selectedReviewAction: run.selectedReviewAction || null,
|
|
4064
4144
|
completedDeliveryActionId: run.completedDeliveryActionId ?? null,
|
|
@@ -4444,6 +4524,7 @@ class AiHubServer {
|
|
|
4444
4524
|
runDiscriminant: persistedRun?.runDiscriminant || undefined,
|
|
4445
4525
|
artifacts: Array.isArray(conversation.artifacts) ? conversation.artifacts : [],
|
|
4446
4526
|
reviewHandoff: conversation.reviewHandoff || null,
|
|
4527
|
+
proposalSet: conversation.proposalSet || null,
|
|
4447
4528
|
pendingDeliveryActionId: conversation.pendingDeliveryActionId ?? null,
|
|
4448
4529
|
selectedReviewAction: normalizeReviewAction(conversation.selectedReviewAction) || null,
|
|
4449
4530
|
completedDeliveryActionId: conversation.completedDeliveryActionId ?? null,
|
|
@@ -5128,7 +5209,9 @@ class AiHubServer {
|
|
|
5128
5209
|
// jobId (resolveHubJob would never match a UUID, leaving a freeform run
|
|
5129
5210
|
// unpromoted and then dropping its phase signals).
|
|
5130
5211
|
this.maybePromoteFreeformRunToJob(run, signal.jobName || signal.jobId);
|
|
5131
|
-
applySeekMentoringSignal(run, signal)
|
|
5212
|
+
applySeekMentoringSignal(run, signal, (reason) => {
|
|
5213
|
+
this.recordHostEvent(run, run.hostId, { hostError: 'Review handoff was not applied: ' + reason }, 'stderr');
|
|
5214
|
+
});
|
|
5132
5215
|
if (signal.phaseId === 'retrospective' && signal.phaseStatus === 'complete') {
|
|
5133
5216
|
void this.releaseBrowserConversationPage(run.conversationId || run.id);
|
|
5134
5217
|
}
|
|
@@ -5645,12 +5728,39 @@ class AiHubServer {
|
|
|
5645
5728
|
const projectPath = resolveProjectPath(req);
|
|
5646
5729
|
const entries = (0, learning_context_builder_1.readPreservedLearnings)(projectPath, userEmail, scope, level);
|
|
5647
5730
|
const threshold = (0, learning_context_builder_1.getScoreThreshold)(projectPath);
|
|
5648
|
-
|
|
5731
|
+
// Issue #1755 R9: the domain catalogue travels with the corpus so the library's
|
|
5732
|
+
// domain filter and the re-scope selector offer every valid domain, not only the
|
|
5733
|
+
// ones that happen to have an entry already.
|
|
5734
|
+
return res.json({ scope, level, entries, threshold, domains: ['global', ...learning_domains_1.LEARNING_DOMAINS] });
|
|
5649
5735
|
}
|
|
5650
5736
|
catch (error) {
|
|
5651
5737
|
return res.status(500).json({ error: error instanceof Error ? error.message : 'Could not read learnings.' });
|
|
5652
5738
|
}
|
|
5653
5739
|
});
|
|
5740
|
+
// Issue #1857 R19: what the manager said no to, and why.
|
|
5741
|
+
//
|
|
5742
|
+
// A rejection is one of the three lifecycle states, and until now it was the
|
|
5743
|
+
// only one with no way to see it short of opening a markdown file by hand:
|
|
5744
|
+
// `readPreservedLearnings` reads family files, and a rejected claim is
|
|
5745
|
+
// deliberately not in one. A manager who cannot review their own refusals
|
|
5746
|
+
// cannot tell a claim that keeps coming back from one they already settled.
|
|
5747
|
+
//
|
|
5748
|
+
// Manager-only by construction. This is an HTTP read the Hub makes; nothing
|
|
5749
|
+
// in the employee's learning context calls `readRejectedProposals`, and
|
|
5750
|
+
// `test-1857-context-boundary.ts` pins that it never will. Feeding refused
|
|
5751
|
+
// claims back to a job is the influence R10 exists to prevent.
|
|
5752
|
+
this.app.get('/api/ai-hub/rejections', async (req, res) => {
|
|
5753
|
+
try {
|
|
5754
|
+
const userEmail = await this.resolveLearningIdentity('manager');
|
|
5755
|
+
if (!userEmail)
|
|
5756
|
+
return res.json({ entries: [] });
|
|
5757
|
+
const projectPath = resolveProjectPath(req);
|
|
5758
|
+
return res.json({ entries: (0, learning_context_builder_1.readRejectedProposals)(projectPath, userEmail) });
|
|
5759
|
+
}
|
|
5760
|
+
catch (error) {
|
|
5761
|
+
return res.status(500).json({ error: error instanceof Error ? error.message : 'Could not read rejections.' });
|
|
5762
|
+
}
|
|
5763
|
+
});
|
|
5654
5764
|
// #533 §3: add / edit / delete a single learning entry — writes the real file
|
|
5655
5765
|
// at the section's level (machine for the Manager/Company tabs, project for the
|
|
5656
5766
|
// project workspace). The action targets exactly the card's file.
|
|
@@ -5664,7 +5774,16 @@ class AiHubServer {
|
|
|
5664
5774
|
if (!b.category || !VALID_CATEGORIES.includes(b.category))
|
|
5665
5775
|
return res.status(400).json({ error: `category must be one of: ${VALID_CATEGORIES.join(', ')}` });
|
|
5666
5776
|
const level = (b.level && VALID_LEVELS.includes(b.level)) ? b.level : 'machine';
|
|
5667
|
-
|
|
5777
|
+
// Issue #1755: `domain` names the file the entry lives in today, `targetDomain`
|
|
5778
|
+
// the file it should live in after the write. Without the first, every write
|
|
5779
|
+
// resolved to the undecorated global file and a domain-scoped entry could not be
|
|
5780
|
+
// edited or deleted at all.
|
|
5781
|
+
const ref = {
|
|
5782
|
+
scope: b.scope,
|
|
5783
|
+
level,
|
|
5784
|
+
category: b.category,
|
|
5785
|
+
domain: (0, learning_context_builder_1.normalizeLearningDomain)(b.domain),
|
|
5786
|
+
};
|
|
5668
5787
|
// Issue #750: writes to a personal-scope file need a resolved identity —
|
|
5669
5788
|
// fail explicitly rather than writing to a guessed or empty-string file.
|
|
5670
5789
|
const userEmail = await this.resolveLearningIdentity(b.scope);
|
|
@@ -5677,6 +5796,7 @@ class AiHubServer {
|
|
|
5677
5796
|
severity: b.severity,
|
|
5678
5797
|
title: b.title,
|
|
5679
5798
|
body: b.body,
|
|
5799
|
+
targetDomain: b.targetDomain === undefined ? undefined : (0, learning_context_builder_1.normalizeLearningDomain)(b.targetDomain),
|
|
5680
5800
|
});
|
|
5681
5801
|
return res.json({ ok: true, path: result.path });
|
|
5682
5802
|
}
|
|
@@ -6018,6 +6138,7 @@ class AiHubServer {
|
|
|
6018
6138
|
personaKey: resolveConversationPersonaKey(conversation, bucketKey),
|
|
6019
6139
|
artifacts: Array.isArray(conversation.artifacts) ? conversation.artifacts : [],
|
|
6020
6140
|
reviewHandoff: conversation.reviewHandoff || null,
|
|
6141
|
+
proposalSet: conversation.proposalSet || null,
|
|
6021
6142
|
delegation: conversation.delegation || null,
|
|
6022
6143
|
delegationTaskId: conversation.delegationTaskId || null,
|
|
6023
6144
|
nextJobRecommendations: null,
|
|
@@ -7218,6 +7339,25 @@ class AiHubServer {
|
|
|
7218
7339
|
// Issue #1651 R7/R12: resolve staged attachments into persisted metadata
|
|
7219
7340
|
// (attached to the manager's message) and a host-bound reference suffix.
|
|
7220
7341
|
const { attachments, referenceSuffix: attachmentSuffix } = this.resolveMessageAttachments((0, conversation_store_1.conversationScopeKey)(run.scope, run.projectPath), run.conversationId || run.id, req.body.attachmentIds);
|
|
7342
|
+
// Issue #1857 R18. The manager's prose names each proposal by title, and
|
|
7343
|
+
// `move-artifact-between-levels` states that titles never select updates,
|
|
7344
|
+
// so the job would have to guess which revision a sentence referred to.
|
|
7345
|
+
// This carries the decisions as data alongside that prose, appended the
|
|
7346
|
+
// same way an attachment reference is: the manager still reads their own
|
|
7347
|
+
// words in the thread, and the job records against the id and revision.
|
|
7348
|
+
const proposalDecisions = normalizeProposalDecisions(req.body.proposalDecisions);
|
|
7349
|
+
const FENCE = '\u0060'.repeat(3);
|
|
7350
|
+
// The reason is the manager's own words, so it can contain anything a
|
|
7351
|
+
// person types, including a code fence. JSON escapes quotes and newlines
|
|
7352
|
+
// but not backticks, and a stray fence here would close the block early
|
|
7353
|
+
// and leave the rest of the decisions reading as prose. Escaping them
|
|
7354
|
+
// keeps the block one parseable unit; the agent unescapes nothing, because
|
|
7355
|
+
// a backtick inside a reason carries no meaning it needs.
|
|
7356
|
+
const decisionJson = JSON.stringify(proposalDecisions).split('\u0060').join('\\u0060');
|
|
7357
|
+
const decisionSuffix = proposalDecisions.length
|
|
7358
|
+
? '\n\n<!-- fraim-proposal-decisions -->\n' + FENCE + 'json\n'
|
|
7359
|
+
+ decisionJson + '\n' + FENCE
|
|
7360
|
+
: '';
|
|
7221
7361
|
// No resumable session — start fresh using a handoff prompt so the agent
|
|
7222
7362
|
// picks up from the preserved conversation state + manager coaching.
|
|
7223
7363
|
const reviewApprovalSystemEventText = buildReviewApprovalSystemEventText(prepared.display || message);
|
|
@@ -7266,8 +7406,8 @@ class AiHubServer {
|
|
|
7266
7406
|
this.persistRunConversation(startedFresh, startedFresh.conversationId || startedFresh.id);
|
|
7267
7407
|
this.runRegistry.create(run, {});
|
|
7268
7408
|
const freshLaunch = this.resolveLaunchAgent(run.configuredAgentId, run.hostId);
|
|
7269
|
-
this.recordHubGeneratedEvent(run, 'agent_input', freshPayload.message + attachmentSuffix);
|
|
7270
|
-
const freshChild = this.hostRuntime.startRun(run.hostId, run.projectPath, freshPayload.message + attachmentSuffix, {
|
|
7409
|
+
this.recordHubGeneratedEvent(run, 'agent_input', freshPayload.message + attachmentSuffix + decisionSuffix);
|
|
7410
|
+
const freshChild = this.hostRuntime.startRun(run.hostId, run.projectPath, freshPayload.message + attachmentSuffix + decisionSuffix, {
|
|
7271
7411
|
onEvent: (event, channel) => {
|
|
7272
7412
|
this.runRegistry.update(run.id, (current) => {
|
|
7273
7413
|
if (event.sessionId) {
|
|
@@ -7353,8 +7493,8 @@ class AiHubServer {
|
|
|
7353
7493
|
// them, so any other continueRun call site delivering the turn (or a
|
|
7354
7494
|
// turn that exits before its first event) left the badge stuck
|
|
7355
7495
|
// forever. See clearStaleDeliveryStatuses and handleRunExit.
|
|
7356
|
-
this.recordHubGeneratedEvent(run, 'agent_input', message + attachmentSuffix);
|
|
7357
|
-
const child = this.hostRuntime.continueRun(run.hostId, run.projectPath, run.sessionId, message + attachmentSuffix, {
|
|
7496
|
+
this.recordHubGeneratedEvent(run, 'agent_input', message + attachmentSuffix + decisionSuffix);
|
|
7497
|
+
const child = this.hostRuntime.continueRun(run.hostId, run.projectPath, run.sessionId, message + attachmentSuffix + decisionSuffix, {
|
|
7358
7498
|
onEvent: (event, channel) => {
|
|
7359
7499
|
if (run.hostId === 'codex' && reviewApprovalSystemEventText) {
|
|
7360
7500
|
codexMissingReasoningResumeError = codexMissingReasoningResumeError ||
|