fraim 2.0.270 → 2.0.271
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/src/cli/commands/add-ide.js +28 -2
- package/dist/src/cli/commands/learning-usage.js +412 -0
- package/dist/src/cli/fraim.js +2 -0
- package/dist/src/core/ai-mentor.js +27 -14
- package/dist/src/core/config-loader.js +48 -3
- package/dist/src/core/fraim-config-schema.generated.js +18 -0
- package/dist/src/core/handoff-contracts.js +37 -1
- package/dist/src/core/job-phases.js +2 -14
- package/dist/src/core/resolve-phase-edge.js +75 -0
- package/dist/src/core/types.js +7 -1
- package/dist/src/core/utils/git-utils.js +24 -14
- package/dist/src/core/utils/project-fraim-paths.js +16 -1
- package/dist/src/local-mcp-server/artifact-retention-cleanup.js +8 -0
- package/dist/src/local-mcp-server/learning-context-builder.js +448 -95
- package/dist/src/local-mcp-server/learning-firing-parser.js +247 -0
- package/dist/src/local-mcp-server/learning-usage-analysis.js +347 -0
- package/dist/src/local-mcp-server/learning-usage-attestation.js +191 -0
- package/dist/src/local-mcp-server/learning-usage-projection.js +79 -0
- package/dist/src/local-mcp-server/learning-usage-store.js +417 -0
- package/dist/src/local-mcp-server/stdio-server.js +43 -0
- package/package.json +1 -1
|
@@ -3,14 +3,40 @@
|
|
|
3
3
|
* FRAIM Configuration Loader
|
|
4
4
|
* Loads and validates workspace FRAIM config with fallback to defaults.
|
|
5
5
|
*/
|
|
6
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
7
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
8
|
+
};
|
|
6
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.resetDetectedDefaultBranchCache = resetDetectedDefaultBranchCache;
|
|
7
11
|
exports.normalizeFraimConfig = normalizeFraimConfig;
|
|
8
12
|
exports.loadFraimConfig = loadFraimConfig;
|
|
9
13
|
exports.getConfigValue = getConfigValue;
|
|
10
14
|
exports.getRepositoryInfo = getRepositoryInfo;
|
|
11
15
|
const fs_1 = require("fs");
|
|
16
|
+
const path_1 = __importDefault(require("path"));
|
|
12
17
|
const types_1 = require("./types");
|
|
13
18
|
const project_fraim_paths_1 = require("./utils/project-fraim-paths");
|
|
19
|
+
const git_utils_1 = require("./utils/git-utils");
|
|
20
|
+
/**
|
|
21
|
+
* Issue #1164: detection shells out to git, and config normalization runs on every
|
|
22
|
+
* load, so answers are cached. The cache is keyed by the directory inspected, not
|
|
23
|
+
* global: a long-lived process such as the Hub server normalizes configs for
|
|
24
|
+
* several projects, and a single cached answer would apply one repository's
|
|
25
|
+
* default branch to another. `null` is a real, cacheable result meaning "no
|
|
26
|
+
* default branch could be determined for this directory".
|
|
27
|
+
*/
|
|
28
|
+
const detectedDefaultBranchByDir = new Map();
|
|
29
|
+
function detectDefaultBranchOnce(detectionCwd) {
|
|
30
|
+
const key = detectionCwd ? path_1.default.resolve(detectionCwd) : '';
|
|
31
|
+
if (!detectedDefaultBranchByDir.has(key)) {
|
|
32
|
+
detectedDefaultBranchByDir.set(key, (0, git_utils_1.getDefaultBranch)(detectionCwd));
|
|
33
|
+
}
|
|
34
|
+
return detectedDefaultBranchByDir.get(key) ?? null;
|
|
35
|
+
}
|
|
36
|
+
/** Test seam: lets a test exercise both detection outcomes without a real repo. */
|
|
37
|
+
function resetDetectedDefaultBranchCache() {
|
|
38
|
+
detectedDefaultBranchByDir.clear();
|
|
39
|
+
}
|
|
14
40
|
function normalizeCustomerCommunication(config) {
|
|
15
41
|
const current = config?.['customer-communication'];
|
|
16
42
|
if (!current || typeof current !== 'object')
|
|
@@ -91,7 +117,7 @@ function normalizeAutomation(config) {
|
|
|
91
117
|
}
|
|
92
118
|
};
|
|
93
119
|
}
|
|
94
|
-
function normalizeFraimConfig(config) {
|
|
120
|
+
function normalizeFraimConfig(config, options = {}) {
|
|
95
121
|
// Handle backward compatibility and migration
|
|
96
122
|
const mergedConfig = {
|
|
97
123
|
project: {
|
|
@@ -106,7 +132,9 @@ function normalizeFraimConfig(config) {
|
|
|
106
132
|
provider: config.git.provider || 'github',
|
|
107
133
|
owner: config.git.repoOwner,
|
|
108
134
|
name: config.git.repoName,
|
|
109
|
-
|
|
135
|
+
// Issue #1164: no `|| 'main'`. An undeclared default branch is
|
|
136
|
+
// resolved from git below, or left absent.
|
|
137
|
+
...(config.git.defaultBranch ? { defaultBranch: config.git.defaultBranch } : {})
|
|
110
138
|
} : {})
|
|
111
139
|
},
|
|
112
140
|
customizations: {
|
|
@@ -114,6 +142,19 @@ function normalizeFraimConfig(config) {
|
|
|
114
142
|
...(config.customizations || {})
|
|
115
143
|
}
|
|
116
144
|
};
|
|
145
|
+
// Issue #1164: an undeclared default branch is resolved from the repository
|
|
146
|
+
// itself rather than guessed. When detection fails the field stays absent, so
|
|
147
|
+
// a consumer renders a visible placeholder instead of silently targeting a
|
|
148
|
+
// branch that may not exist.
|
|
149
|
+
if (mergedConfig.repository && !mergedConfig.repository.defaultBranch) {
|
|
150
|
+
const detected = detectDefaultBranchOnce(options.detectionCwd);
|
|
151
|
+
if (detected) {
|
|
152
|
+
mergedConfig.repository.defaultBranch = detected;
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
delete mergedConfig.repository.defaultBranch;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
117
158
|
if (config.customizations?.postCleanupHook || config.customizations?.cleanupCommand) {
|
|
118
159
|
if (!mergedConfig.customizations)
|
|
119
160
|
mergedConfig.customizations = {};
|
|
@@ -158,7 +199,11 @@ function loadFraimConfig(configPath = (0, project_fraim_paths_1.getWorkspaceConf
|
|
|
158
199
|
try {
|
|
159
200
|
const configContent = (0, fs_1.readFileSync)(configPath, 'utf-8');
|
|
160
201
|
const config = JSON.parse(configContent);
|
|
161
|
-
|
|
202
|
+
// Issue #1164: detection must inspect the workspace this config belongs to,
|
|
203
|
+
// not whatever directory the process happens to be running in. The config
|
|
204
|
+
// lives at <workspaceRoot>/fraim/config.json.
|
|
205
|
+
const workspaceRoot = path_1.default.dirname(path_1.default.dirname(path_1.default.resolve(configPath)));
|
|
206
|
+
const mergedConfig = normalizeFraimConfig(config, { detectionCwd: workspaceRoot });
|
|
162
207
|
console.log(`Loaded FRAIM config from ${displayPath}`);
|
|
163
208
|
if (config.git && !config.repository) {
|
|
164
209
|
console.warn('Deprecated: "git" config detected. Consider migrating to "repository" config.');
|
|
@@ -213,6 +213,20 @@ exports.FRAIM_CONFIG_SCHEMA = {
|
|
|
213
213
|
},
|
|
214
214
|
"globalPath": {
|
|
215
215
|
"kind": "string"
|
|
216
|
+
},
|
|
217
|
+
"usage": {
|
|
218
|
+
"kind": "object",
|
|
219
|
+
"properties": {
|
|
220
|
+
"firingLogLimit": {
|
|
221
|
+
"kind": "number"
|
|
222
|
+
},
|
|
223
|
+
"offerLogLimit": {
|
|
224
|
+
"kind": "number"
|
|
225
|
+
},
|
|
226
|
+
"retirementOfferThreshold": {
|
|
227
|
+
"kind": "number"
|
|
228
|
+
}
|
|
229
|
+
}
|
|
216
230
|
}
|
|
217
231
|
}
|
|
218
232
|
},
|
|
@@ -646,6 +660,10 @@ exports.SUPPORTED_FRAIM_CONFIG_PATHS = [
|
|
|
646
660
|
"learning.lastSynthesisDate",
|
|
647
661
|
"learning.scoreThreshold",
|
|
648
662
|
"learning.globalPath",
|
|
663
|
+
"learning.usage",
|
|
664
|
+
"learning.usage.firingLogLimit",
|
|
665
|
+
"learning.usage.offerLogLimit",
|
|
666
|
+
"learning.usage.retirementOfferThreshold",
|
|
649
667
|
"customer-communication",
|
|
650
668
|
"customer-communication.productName",
|
|
651
669
|
"customer-communication.productUrl",
|
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
19
|
exports.isSubmitPhase = isSubmitPhase;
|
|
20
20
|
exports.isRetrospectivePhase = isRetrospectivePhase;
|
|
21
|
+
exports.isAddressFeedbackCompletion = isAddressFeedbackCompletion;
|
|
21
22
|
exports.isDelegationGraphPhase = isDelegationGraphPhase;
|
|
23
|
+
exports.validateAddressFeedbackApproval = validateAddressFeedbackApproval;
|
|
22
24
|
exports.validateReviewHandoff = validateReviewHandoff;
|
|
23
25
|
exports.validateNextJobRecommendations = validateNextJobRecommendations;
|
|
24
26
|
exports.validateDelegationLedger = validateDelegationLedger;
|
|
@@ -43,6 +45,13 @@ function isSubmitPhase(currentPhase, status) {
|
|
|
43
45
|
function isRetrospectivePhase(currentPhase, status) {
|
|
44
46
|
return currentPhase === 'retrospective' && status === 'complete';
|
|
45
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Returns true when a seekMentoring call is address-feedback completing.
|
|
50
|
+
* Completing address-feedback requires manager approval (evidence.approved: true).
|
|
51
|
+
*/
|
|
52
|
+
function isAddressFeedbackCompletion(currentPhase, status) {
|
|
53
|
+
return currentPhase === 'address-feedback' && status === 'complete';
|
|
54
|
+
}
|
|
46
55
|
/**
|
|
47
56
|
* Returns true when a seekMentoring call is the delegation graph phase.
|
|
48
57
|
* This matches the existing Hub test fixture for the delegation board.
|
|
@@ -53,6 +62,20 @@ function isDelegationGraphPhase(currentPhase) {
|
|
|
53
62
|
// ---------------------------------------------------------------------------
|
|
54
63
|
// Individual validators
|
|
55
64
|
// ---------------------------------------------------------------------------
|
|
65
|
+
/**
|
|
66
|
+
* Validates that evidence.approved is true for address-feedback completion.
|
|
67
|
+
* Returns null if valid, or an array of error strings.
|
|
68
|
+
*/
|
|
69
|
+
function validateAddressFeedbackApproval(evidence) {
|
|
70
|
+
if (evidence.approved !== true) {
|
|
71
|
+
return [
|
|
72
|
+
`evidence.approved must be true when completing address-feedback. ` +
|
|
73
|
+
`Set evidence.approved: true only after the manager has explicitly approved the work. ` +
|
|
74
|
+
`If the manager has not yet approved, set status: "failure" to re-enter the review loop.`,
|
|
75
|
+
];
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
56
79
|
/**
|
|
57
80
|
* Validates evidence.reviewHandoff. Returns null if valid, or an array of
|
|
58
81
|
* human-readable error strings describing what is wrong.
|
|
@@ -213,11 +236,21 @@ function validateHandoffContracts(args) {
|
|
|
213
236
|
if (errors)
|
|
214
237
|
return errors;
|
|
215
238
|
}
|
|
239
|
+
if (isAddressFeedbackCompletion(phase, status)) {
|
|
240
|
+
const errors = validateAddressFeedbackApproval(evidence);
|
|
241
|
+
if (errors)
|
|
242
|
+
return errors;
|
|
243
|
+
}
|
|
216
244
|
return [];
|
|
217
245
|
}
|
|
218
246
|
// ---------------------------------------------------------------------------
|
|
219
247
|
// Rejection message builder
|
|
220
248
|
// ---------------------------------------------------------------------------
|
|
249
|
+
const ADDRESS_FEEDBACK_APPROVAL_SCHEMA = `\`\`\`javascript
|
|
250
|
+
evidence: {
|
|
251
|
+
approved: true // Set only after the manager explicitly approves via the Hub review action
|
|
252
|
+
}
|
|
253
|
+
\`\`\``;
|
|
221
254
|
const REVIEW_HANDOFF_SCHEMA = `\`\`\`javascript
|
|
222
255
|
evidence: {
|
|
223
256
|
reviewHandoff: {
|
|
@@ -257,6 +290,7 @@ evidence: {
|
|
|
257
290
|
}
|
|
258
291
|
\`\`\``;
|
|
259
292
|
const FIELD_SCHEMAS = {
|
|
293
|
+
approved: ADDRESS_FEEDBACK_APPROVAL_SCHEMA,
|
|
260
294
|
reviewHandoff: REVIEW_HANDOFF_SCHEMA,
|
|
261
295
|
nextJobRecommendations: NEXT_JOB_RECOMMENDATIONS_SCHEMA,
|
|
262
296
|
delegationLedger: DELEGATION_LEDGER_SCHEMA,
|
|
@@ -280,6 +314,8 @@ function buildHandoffRejectionMessage(currentPhase, field, errors) {
|
|
|
280
314
|
'',
|
|
281
315
|
schemaHint,
|
|
282
316
|
'',
|
|
283
|
-
|
|
317
|
+
field === 'approved'
|
|
318
|
+
? 'The job is **not** marked complete. Set `evidence.approved: true` (only after explicit manager approval) and resubmit.'
|
|
319
|
+
: `The job is **not** marked complete. Add the \`evidence.${field}\` object and resubmit this phase.`,
|
|
284
320
|
].join('\n');
|
|
285
321
|
}
|
|
@@ -9,6 +9,7 @@ exports.labelForPhaseId = labelForPhaseId;
|
|
|
9
9
|
const fs_1 = __importDefault(require("fs"));
|
|
10
10
|
const path_1 = __importDefault(require("path"));
|
|
11
11
|
const project_fraim_paths_1 = require("./utils/project-fraim-paths");
|
|
12
|
+
const resolve_phase_edge_1 = require("./resolve-phase-edge");
|
|
12
13
|
const EMPLOYEE_JOB_LAYERS = [
|
|
13
14
|
{ segments: ['ai-employee', 'jobs'] },
|
|
14
15
|
{ segments: ['personalized-employee', 'jobs'] },
|
|
@@ -99,19 +100,6 @@ function findJobStubPath(projectPath, jobId) {
|
|
|
99
100
|
}
|
|
100
101
|
return null;
|
|
101
102
|
}
|
|
102
|
-
function nextPhase(edge, discriminant) {
|
|
103
|
-
if (edge == null)
|
|
104
|
-
return null;
|
|
105
|
-
if (typeof edge === 'string')
|
|
106
|
-
return edge;
|
|
107
|
-
if (typeof edge === 'object') {
|
|
108
|
-
if (typeof edge[discriminant] === 'string')
|
|
109
|
-
return edge[discriminant];
|
|
110
|
-
if (typeof edge.default === 'string')
|
|
111
|
-
return edge.default;
|
|
112
|
-
}
|
|
113
|
-
return null;
|
|
114
|
-
}
|
|
115
103
|
function loadJobPhasesFromSteps(filePath) {
|
|
116
104
|
const raw = fs_1.default.readFileSync(filePath, 'utf8');
|
|
117
105
|
const stepsMatch = raw.match(/## Steps\r?\n([\s\S]*?)(?:\r?\n## |\r?\n---|$)/);
|
|
@@ -141,7 +129,7 @@ function loadJobPhases(jobId, projectPath, discriminant = 'feature') {
|
|
|
141
129
|
const phaseDef = fm.phases[cursor];
|
|
142
130
|
if (!phaseDef)
|
|
143
131
|
break;
|
|
144
|
-
cursor =
|
|
132
|
+
cursor = (0, resolve_phase_edge_1.resolvePhaseEdge)(phaseDef.onSuccess, discriminant);
|
|
145
133
|
}
|
|
146
134
|
const labels = fm.phaseLabels || {};
|
|
147
135
|
return ordered.map((id) => ({ id, label: friendlyPhaseLabel(id, labels[id]) }));
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Single authority for resolving a job phase transition (issue #1123).
|
|
4
|
+
*
|
|
5
|
+
* A phase edge (`onSuccess` or `onFailure`) is either a phase id, a discriminant
|
|
6
|
+
* map, or terminal. Before this module the identical resolver existed twice, in
|
|
7
|
+
* `src/core/job-phases.ts` and `src/ai-hub/catalog.ts`, and the discriminant
|
|
8
|
+
* precedence lived a third time inline in `src/core/ai-mentor.ts`. Three copies of
|
|
9
|
+
* one rule is how the success and failure paths drift apart, which is exactly the
|
|
10
|
+
* class of defect this issue is about.
|
|
11
|
+
*
|
|
12
|
+
* Both edges accept a map. A map must declare `default`; an unset or unrecognised
|
|
13
|
+
* discriminant resolves to it, so a job can never route somewhere unexpected. A
|
|
14
|
+
* string edge never consults a discriminant at all, which is what makes this
|
|
15
|
+
* change inert for every job that authors no map.
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.resolvePhaseEdge = resolvePhaseEdge;
|
|
19
|
+
exports.resolveDiscriminant = resolveDiscriminant;
|
|
20
|
+
exports.discriminantKeys = discriminantKeys;
|
|
21
|
+
/** The default discriminant, and the mandatory key on every authored map. */
|
|
22
|
+
const DEFAULT_DISCRIMINANT = 'default';
|
|
23
|
+
/**
|
|
24
|
+
* Resolve an edge to the next phase id.
|
|
25
|
+
*
|
|
26
|
+
* Returns `null` when the edge is terminal, when a map resolves to nothing, or
|
|
27
|
+
* when the edge is malformed. Callers decide what a `null` means for them:
|
|
28
|
+
* `loadJobPhases` stops walking, `ai-mentor` falls back to self-retry.
|
|
29
|
+
*/
|
|
30
|
+
function resolvePhaseEdge(edge, discriminant) {
|
|
31
|
+
if (edge == null)
|
|
32
|
+
return null;
|
|
33
|
+
if (typeof edge === 'string')
|
|
34
|
+
return edge;
|
|
35
|
+
if (typeof edge === 'object') {
|
|
36
|
+
if (typeof edge[discriminant] === 'string')
|
|
37
|
+
return edge[discriminant];
|
|
38
|
+
if (typeof edge[DEFAULT_DISCRIMINANT] === 'string')
|
|
39
|
+
return edge[DEFAULT_DISCRIMINANT];
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The discriminant an agent supplied on a `seekMentoring` call.
|
|
45
|
+
*
|
|
46
|
+
* `phaseOutcome` is the general form and `issueType` the original narrower one;
|
|
47
|
+
* both are accepted from either `findings` or `evidence` so an agent following
|
|
48
|
+
* older phase text still routes correctly. This precedence is deliberately shared
|
|
49
|
+
* by the success and failure paths: if they read the discriminant differently,
|
|
50
|
+
* the same call would route two ways depending on its status.
|
|
51
|
+
*/
|
|
52
|
+
function resolveDiscriminant(findings, evidence) {
|
|
53
|
+
const read = (source, key) => {
|
|
54
|
+
if (!source || typeof source !== 'object')
|
|
55
|
+
return null;
|
|
56
|
+
const value = source[key];
|
|
57
|
+
return typeof value === 'string' && value.length > 0 ? value : null;
|
|
58
|
+
};
|
|
59
|
+
return (read(findings, 'phaseOutcome')
|
|
60
|
+
?? read(findings, 'issueType')
|
|
61
|
+
?? read(evidence, 'issueType')
|
|
62
|
+
?? read(evidence, 'phaseOutcome')
|
|
63
|
+
?? DEFAULT_DISCRIMINANT);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The discriminant keys a map edge offers, excluding `default`.
|
|
67
|
+
*
|
|
68
|
+
* Used to tell the agent which outcomes a phase accepts. A routing map nobody
|
|
69
|
+
* knows how to trigger is decorative, which is the failure recorded as #1135.
|
|
70
|
+
*/
|
|
71
|
+
function discriminantKeys(edge) {
|
|
72
|
+
if (!edge || typeof edge !== 'object')
|
|
73
|
+
return [];
|
|
74
|
+
return Object.keys(edge).filter((key) => key !== DEFAULT_DISCRIMINANT);
|
|
75
|
+
}
|
package/dist/src/core/types.js
CHANGED
|
@@ -17,7 +17,13 @@ exports.DEFAULT_FRAIM_CONFIG = {
|
|
|
17
17
|
provider: 'github',
|
|
18
18
|
owner: '',
|
|
19
19
|
name: '',
|
|
20
|
-
|
|
20
|
+
// Issue #1164: deliberately no defaultBranch. `owner` and `name` default to
|
|
21
|
+
// the empty string, which is obviously missing and fails loudly downstream.
|
|
22
|
+
// A default branch name does not have that property: `'main'` is
|
|
23
|
+
// indistinguishable from a real answer, so it silently targeted pull
|
|
24
|
+
// requests at a branch this repository does not have. An absent value is
|
|
25
|
+
// resolved from git in `normalizeFraimConfig`, and left absent when it
|
|
26
|
+
// cannot be determined.
|
|
21
27
|
},
|
|
22
28
|
customizations: {}
|
|
23
29
|
};
|
|
@@ -77,6 +77,9 @@ function getPort() {
|
|
|
77
77
|
* Determines the database name based on the git branch
|
|
78
78
|
*/
|
|
79
79
|
function determineDatabaseName() {
|
|
80
|
+
if (process.env.MONGODB_DB_NAME) {
|
|
81
|
+
return process.env.MONGODB_DB_NAME;
|
|
82
|
+
}
|
|
80
83
|
try {
|
|
81
84
|
const branchName = process.env.FRAIM_BRANCH || process.env.FRAIM_BRANCH_NAME || (0, child_process_1.execSync)('git rev-parse --abbrev-ref HEAD').toString().trim();
|
|
82
85
|
const issueMatch = branchName.match(/issue-(\d+)/i) || branchName.match(/(\d+)-/);
|
|
@@ -87,7 +90,7 @@ function determineDatabaseName() {
|
|
|
87
90
|
catch (e) {
|
|
88
91
|
// Silently fail
|
|
89
92
|
}
|
|
90
|
-
return process.env.
|
|
93
|
+
return process.env.NODE_ENV === 'production' ? 'fraim_prod' : 'fraim_dev';
|
|
91
94
|
}
|
|
92
95
|
/**
|
|
93
96
|
* Gets the current git branch name
|
|
@@ -114,14 +117,28 @@ function determineSchema(branchName) {
|
|
|
114
117
|
return 'prod';
|
|
115
118
|
}
|
|
116
119
|
/**
|
|
117
|
-
* Gets the default branch name from git remote
|
|
120
|
+
* Gets the default branch name from the git remote, or `null` when it cannot be
|
|
121
|
+
* determined.
|
|
122
|
+
*
|
|
123
|
+
* Issue #1164: this deliberately reports failure instead of guessing. A branch
|
|
124
|
+
* name is used to target pull requests, so a wrong-but-plausible answer is worse
|
|
125
|
+
* than no answer: it opens a PR against a branch that may not exist. Two earlier
|
|
126
|
+
* fallbacks are removed for that reason:
|
|
127
|
+
*
|
|
128
|
+
* - falling back to the *current* branch, which reports whatever feature branch
|
|
129
|
+
* happens to be checked out as though it were the repository default;
|
|
130
|
+
* - falling back to the literal `'main'`, which is indistinguishable from a real
|
|
131
|
+
* detection result.
|
|
132
|
+
*
|
|
133
|
+
* @param cwd Directory to inspect. Defaults to the current working directory.
|
|
134
|
+
* Explicit so callers and tests can scope detection to a known repo.
|
|
118
135
|
*/
|
|
119
|
-
function getDefaultBranch() {
|
|
136
|
+
function getDefaultBranch(cwd) {
|
|
120
137
|
try {
|
|
121
|
-
// Try to get the default branch from remote HEAD
|
|
122
138
|
const remoteHead = (0, child_process_1.execSync)('git symbolic-ref refs/remotes/origin/HEAD', {
|
|
123
139
|
timeout: 2000, // 2 second timeout
|
|
124
|
-
stdio: 'pipe'
|
|
140
|
+
stdio: 'pipe',
|
|
141
|
+
...(cwd ? { cwd } : {})
|
|
125
142
|
}).toString().trim();
|
|
126
143
|
const match = remoteHead.match(/refs\/remotes\/origin\/(.+)$/);
|
|
127
144
|
if (match) {
|
|
@@ -129,16 +146,9 @@ function getDefaultBranch() {
|
|
|
129
146
|
}
|
|
130
147
|
}
|
|
131
148
|
catch (e) {
|
|
132
|
-
//
|
|
133
|
-
try {
|
|
134
|
-
return getCurrentGitBranch();
|
|
135
|
-
}
|
|
136
|
-
catch (e2) {
|
|
137
|
-
// Fall back to common defaults
|
|
138
|
-
}
|
|
149
|
+
// No remote HEAD to read: a bare local repo, no origin, or not a repo at all.
|
|
139
150
|
}
|
|
140
|
-
|
|
141
|
-
return 'main';
|
|
151
|
+
return null;
|
|
142
152
|
}
|
|
143
153
|
/**
|
|
144
154
|
* Sanitizes a repository identifier by stripping local paths and normalizing to standard HTTPS URLs.
|
|
@@ -144,9 +144,24 @@ function getUserFraimDisplayPath(relativePath = '') {
|
|
|
144
144
|
/**
|
|
145
145
|
* Get the user-level FRAIM directory (~/.fraim/).
|
|
146
146
|
* Can be overridden with FRAIM_USER_DIR env var for testing.
|
|
147
|
+
*
|
|
148
|
+
* Issue #1159: inside a sandboxed test run the silent fallback to the real home
|
|
149
|
+
* is a data-loss bug, not a convenience. A test suite that reached `~/.fraim`
|
|
150
|
+
* booted `AiHubServer` instances against the developer's live conversation
|
|
151
|
+
* store and parked four in-flight runs as "Waiting on you". When the runner has
|
|
152
|
+
* marked the run sandboxed, an unset `FRAIM_USER_DIR` means isolation was
|
|
153
|
+
* broken, so fail here rather than corrupt real state.
|
|
147
154
|
*/
|
|
148
155
|
function getUserFraimDirPath() {
|
|
149
|
-
|
|
156
|
+
const override = process.env.FRAIM_USER_DIR;
|
|
157
|
+
if (override)
|
|
158
|
+
return override;
|
|
159
|
+
if (process.env.FRAIM_TEST_SANDBOX === '1') {
|
|
160
|
+
throw new Error('FRAIM_USER_DIR is unset inside a sandboxed test run (FRAIM_TEST_SANDBOX=1). '
|
|
161
|
+
+ 'Refusing to fall back to the real ~/.fraim. Restore FRAIM_USER_DIR to the run sandbox '
|
|
162
|
+
+ 'before touching the machine-level FRAIM home.');
|
|
163
|
+
}
|
|
164
|
+
return (0, path_1.join)(os_1.default.homedir(), '.fraim');
|
|
150
165
|
}
|
|
151
166
|
function getUserFraimLearningsDir() {
|
|
152
167
|
return (0, path_1.join)(getUserFraimDirPath(), 'personalized-employee', 'learnings');
|
|
@@ -17,6 +17,14 @@ exports.ARTIFACT_RETENTION_CATEGORIES = [
|
|
|
17
17
|
'evidence',
|
|
18
18
|
'feedback',
|
|
19
19
|
'cleanup_manifests',
|
|
20
|
+
// Issue #1103 R18: the usage record is personal-bearing behavioural metadata, so
|
|
21
|
+
// it needs a storage-limitation category like every other retained artifact.
|
|
22
|
+
// Unlike the others it is one JSON file rather than a directory of files, so the
|
|
23
|
+
// value bounds how long an individual offer or firing record keeps its detail
|
|
24
|
+
// (applied by learning-usage-store.pruneUsageStore) rather than when a file is
|
|
25
|
+
// deleted. Aggregate counters are never pruned; deleting them would misreport
|
|
26
|
+
// history rather than minimise it.
|
|
27
|
+
'learning_usage',
|
|
20
28
|
];
|
|
21
29
|
function readWorkspaceConfig(workspaceRoot) {
|
|
22
30
|
try {
|