fraim 2.0.317 → 2.0.319
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/first-run.js +41 -6
- package/dist/src/cli/doctor/checks/mcp-connectivity-checks.js +4 -17
- package/dist/src/cli/mcp/fraim-mcp-latest-launcher.js +11 -1
- package/dist/src/cli/utils/win32-cmd-spawn.js +38 -0
- package/dist/src/config/persona-capability-bundles.js +1 -1
- package/dist/src/core/ai-mentor.js +57 -7
- package/dist/src/core/job-phases.js +16 -1
- package/dist/src/core/quality-evidence.js +10 -0
- package/dist/src/core/resolve-phase-edge.js +20 -2
- package/dist/src/first-run/session-service.js +21 -6
- package/dist/src/local-mcp-server/learning-context-builder.js +8 -5
- package/dist/src/local-mcp-server/learning-usage-attestation.js +31 -8
- package/dist/src/mcp/tool-schemas.js +1 -1
- package/dist/src/services/demo-seed-service.js +1 -0
- package/package.json +1 -1
|
@@ -1,4 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
37
|
};
|
|
@@ -7,9 +40,6 @@ exports.firstRunCommand = exports.runFirstRun = void 0;
|
|
|
7
40
|
const commander_1 = require("commander");
|
|
8
41
|
const chalk_1 = __importDefault(require("chalk"));
|
|
9
42
|
const child_process_1 = require("child_process");
|
|
10
|
-
const server_1 = require("../../first-run/server");
|
|
11
|
-
const session_service_1 = require("../../first-run/session-service");
|
|
12
|
-
const ports_1 = require("../../core/utils/ports");
|
|
13
43
|
function openBrowser(url) {
|
|
14
44
|
try {
|
|
15
45
|
if (process.platform === 'win32') {
|
|
@@ -33,17 +63,22 @@ function openBrowser(url) {
|
|
|
33
63
|
}
|
|
34
64
|
}
|
|
35
65
|
const runFirstRun = async (options) => {
|
|
66
|
+
const [{ FirstRunServer }, { FirstRunSessionService }, { findAvailablePort }] = await Promise.all([
|
|
67
|
+
Promise.resolve().then(() => __importStar(require('../../first-run/server'))),
|
|
68
|
+
Promise.resolve().then(() => __importStar(require('../../first-run/session-service'))),
|
|
69
|
+
Promise.resolve().then(() => __importStar(require('../../core/utils/ports'))),
|
|
70
|
+
]);
|
|
36
71
|
// Issue #646: the key is optional. When launched by the no-terminal macOS
|
|
37
72
|
// installer (.pkg), no key is passed — the wizard prompts the user to paste it.
|
|
38
73
|
const key = options.key || process.env.FRAIM_API_KEY || process.env.FRAIM_SETUP_KEY || process.env.FRAIM_INSTALL_KEY || '';
|
|
39
|
-
const sessionService = new
|
|
74
|
+
const sessionService = new FirstRunSessionService({
|
|
40
75
|
key,
|
|
41
76
|
headless: options.headless,
|
|
42
77
|
resume: options.resume,
|
|
43
78
|
projectRoot: options.projectRoot,
|
|
44
79
|
});
|
|
45
|
-
const server = new
|
|
46
|
-
const port = await
|
|
80
|
+
const server = new FirstRunServer({ sessionService });
|
|
81
|
+
const port = await findAvailablePort(43120);
|
|
47
82
|
const url = `http://127.0.0.1:${port}/first-run/`;
|
|
48
83
|
await server.start(port);
|
|
49
84
|
console.log(chalk_1.default.blue('Starting FRAIM first-run...'));
|
|
@@ -52,6 +52,7 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
52
52
|
const path_1 = __importDefault(require("path"));
|
|
53
53
|
const os_1 = __importDefault(require("os"));
|
|
54
54
|
const child_process_1 = require("child_process");
|
|
55
|
+
const win32_cmd_spawn_1 = require("../../utils/win32-cmd-spawn");
|
|
55
56
|
const axios_1 = __importDefault(require("axios"));
|
|
56
57
|
const toml = __importStar(require("toml"));
|
|
57
58
|
const ide_detector_1 = require("../../setup/ide-detector");
|
|
@@ -673,23 +674,9 @@ async function spawnAndHandshake(command, args, timeoutMs = STDIO_HANDSHAKE_TIME
|
|
|
673
674
|
env: process.env
|
|
674
675
|
};
|
|
675
676
|
if (process.platform === 'win32') {
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
// The outer pair of double-quotes is mandatory when the first token is quoted.
|
|
680
|
-
// We must also pass windowsVerbatimArguments: true so Node's CreateProcess call
|
|
681
|
-
// does not re-escape our already-quoted command string.
|
|
682
|
-
const comSpec = process.env.ComSpec || 'cmd.exe';
|
|
683
|
-
const quotedTokens = [command, ...args].map((a) => {
|
|
684
|
-
const v = String(a);
|
|
685
|
-
return /[\s"&|<>^]/.test(v) ? `"${v.replace(/"/g, '""')}"` : v;
|
|
686
|
-
});
|
|
687
|
-
const innerCmd = quotedTokens.join(' ');
|
|
688
|
-
// Wrap in outer quotes only when the command contains spaces (i.e. was itself quoted).
|
|
689
|
-
const needsOuterQuote = /[\s"&|<>^]/.test(String(command));
|
|
690
|
-
const cmdString = needsOuterQuote ? `"${innerCmd}"` : innerCmd;
|
|
691
|
-
spawnCommand = comSpec;
|
|
692
|
-
spawnArgs = ['/d', '/s', '/c', cmdString];
|
|
677
|
+
const built = (0, win32_cmd_spawn_1.buildCmdExeArgs)(command, args.map(String));
|
|
678
|
+
spawnCommand = built.command;
|
|
679
|
+
spawnArgs = built.args;
|
|
693
680
|
spawnOptions = { ...spawnOptions, windowsVerbatimArguments: true };
|
|
694
681
|
}
|
|
695
682
|
else {
|
|
@@ -138,6 +138,16 @@ function runtimeFromEnv(env = process.env) {
|
|
|
138
138
|
? { executable: env.FRAIM_PACKAGED_CLI_EXECUTABLE, script: env.FRAIM_PACKAGED_CLI_SCRIPT }
|
|
139
139
|
: null;
|
|
140
140
|
}
|
|
141
|
+
// Regular Node.js (and ELECTRON_RUN_AS_NODE) cannot see inside Electron's
|
|
142
|
+
// .asar archives. For paths like …/app.asar/node_modules/…, verify the
|
|
143
|
+
// archive file itself rather than the internal entry.
|
|
144
|
+
function existsOrAsarExists(filePath) {
|
|
145
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
146
|
+
const asarSep = normalized.indexOf('.asar/');
|
|
147
|
+
if (asarSep === -1)
|
|
148
|
+
return fs_1.default.existsSync(filePath);
|
|
149
|
+
return fs_1.default.existsSync(filePath.slice(0, asarSep + '.asar'.length));
|
|
150
|
+
}
|
|
141
151
|
function getPackagedFraimRuntime(env = process.env) {
|
|
142
152
|
const envRuntime = runtimeFromEnv(env);
|
|
143
153
|
if (envRuntime)
|
|
@@ -146,7 +156,7 @@ function getPackagedFraimRuntime(env = process.env) {
|
|
|
146
156
|
const raw = JSON.parse(fs_1.default.readFileSync(getPackagedFraimRuntimePath(), 'utf8'));
|
|
147
157
|
if (!raw.executable || !raw.script)
|
|
148
158
|
return null;
|
|
149
|
-
if (!fs_1.default.existsSync(raw.executable) || !
|
|
159
|
+
if (!fs_1.default.existsSync(raw.executable) || !existsOrAsarExists(raw.script))
|
|
150
160
|
return null;
|
|
151
161
|
return { executable: raw.executable, script: raw.script };
|
|
152
162
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.spawnViaCmdExe = spawnViaCmdExe;
|
|
4
|
+
exports.buildCmdExeArgs = buildCmdExeArgs;
|
|
5
|
+
const child_process_1 = require("child_process");
|
|
6
|
+
function quoteCmdToken(s) {
|
|
7
|
+
return /[\s"&|<>^]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Spawn a command through cmd.exe on Windows so .cmd shims resolve correctly.
|
|
11
|
+
* Applies outer-quote wrapping and windowsVerbatimArguments:true so paths
|
|
12
|
+
* containing spaces are not double-escaped by Node's CreateProcess call.
|
|
13
|
+
*/
|
|
14
|
+
function spawnViaCmdExe(command, args, options) {
|
|
15
|
+
const comSpec = process.env.ComSpec || 'cmd.exe';
|
|
16
|
+
const tokens = [command, ...args].map(quoteCmdToken);
|
|
17
|
+
const innerCmd = tokens.join(' ');
|
|
18
|
+
const needsOuterQuote = /[\s"&|<>^]/.test(command);
|
|
19
|
+
const cmdString = needsOuterQuote ? `"${innerCmd}"` : innerCmd;
|
|
20
|
+
return (0, child_process_1.spawnSync)(comSpec, ['/d', '/s', '/c', cmdString], {
|
|
21
|
+
...options,
|
|
22
|
+
windowsVerbatimArguments: true,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Build cmd.exe args for async spawn callers (e.g. mcp-connectivity-checks).
|
|
27
|
+
* Returns the command, args, and a verbatim flag to set windowsVerbatimArguments: true.
|
|
28
|
+
*/
|
|
29
|
+
function buildCmdExeArgs(command, args) {
|
|
30
|
+
const tokens = [command, ...args].map(quoteCmdToken);
|
|
31
|
+
const innerCmd = tokens.join(' ');
|
|
32
|
+
const needsOuterQuote = /[\s"&|<>^]/.test(command);
|
|
33
|
+
return {
|
|
34
|
+
command: process.env.ComSpec || 'cmd.exe',
|
|
35
|
+
args: ['/d', '/s', '/c', needsOuterQuote ? `"${innerCmd}"` : innerCmd],
|
|
36
|
+
verbatim: true,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -72,7 +72,7 @@ exports.PERSONA_CAPABILITY_BUNDLES = {
|
|
|
72
72
|
personaKey: 'qasm',
|
|
73
73
|
bundleId: 'persona-qasm-core',
|
|
74
74
|
catalogMetadata: buildCatalogMetadata('qasm', ['test-authoring', 'iterative-quality-improvement', 'ui-polish-validation']),
|
|
75
|
-
protectedJobs: ['test-authoring', 'ui-polish-validation', 'codebase-quality-assessment', 'test-quality-assessment', 'user-testing-and-bug-bash', 'iterative-quality-improvement', 'accessibility-audit', 'api-testing', 'performance-
|
|
75
|
+
protectedJobs: ['test-authoring', 'ui-polish-validation', 'codebase-quality-assessment', 'test-quality-assessment', 'user-testing-and-bug-bash', 'iterative-quality-improvement', 'accessibility-audit', 'api-testing', 'stress-test-app-under-load', 'performance-quality-assessment'],
|
|
76
76
|
protectedAliases: ['qa', 'quality-assurance'],
|
|
77
77
|
defaultHireMode: 'job',
|
|
78
78
|
lockCopy: 'Hire QAsm to unlock QA validation for this request.'
|
|
@@ -26,7 +26,8 @@ class AIMentor {
|
|
|
26
26
|
const phases = workflow.metadata.phases || {};
|
|
27
27
|
const hasMetadata = !!phases[args.currentPhase];
|
|
28
28
|
const hasMarkdown = workflow.phases.has(args.currentPhase);
|
|
29
|
-
|
|
29
|
+
const isRecurrenceMember = !!(0, resolve_phase_edge_1.findRecurrenceForMember)(phases, args.currentPhase);
|
|
30
|
+
if (!hasMetadata && !hasMarkdown && !isRecurrenceMember && args.currentPhase !== 'starting') {
|
|
30
31
|
throw new Error(`Phase "${args.currentPhase}" not found in job "${args.jobName}".`);
|
|
31
32
|
}
|
|
32
33
|
}
|
|
@@ -133,7 +134,11 @@ class AIMentor {
|
|
|
133
134
|
};
|
|
134
135
|
}
|
|
135
136
|
const isVeryFirstCall = phaseId === 'starting';
|
|
136
|
-
|
|
137
|
+
let targetPhase = isVeryFirstCall ? (workflow.metadata.initialPhase || 'starting') : phaseId;
|
|
138
|
+
const targetDefinition = workflow.metadata.phases?.[targetPhase];
|
|
139
|
+
if ((0, resolve_phase_edge_1.isRecurrencePhase)(targetDefinition)) {
|
|
140
|
+
targetPhase = isVeryFirstCall ? targetDefinition.setup.phase : targetDefinition.recur.phase;
|
|
141
|
+
}
|
|
137
142
|
let message = `### Current Phase: ${targetPhase}\n\n`;
|
|
138
143
|
let instructions = workflow.phases.get(targetPhase);
|
|
139
144
|
if (instructions) {
|
|
@@ -143,7 +148,10 @@ class AIMentor {
|
|
|
143
148
|
else {
|
|
144
149
|
message += `No specific instructions found for phase: ${targetPhase}`;
|
|
145
150
|
}
|
|
146
|
-
const
|
|
151
|
+
const recurrenceMember = (0, resolve_phase_edge_1.findRecurrenceForMember)(workflow.metadata.phases, targetPhase);
|
|
152
|
+
const phaseFlow = recurrenceMember
|
|
153
|
+
? { onSuccess: undefined, onFailure: recurrenceMember.node[recurrenceMember.role].onFailure }
|
|
154
|
+
: ((0, resolve_phase_edge_1.isRecurrencePhase)(workflow.metadata.phases?.[targetPhase]) ? undefined : workflow.metadata.phases?.[targetPhase]);
|
|
147
155
|
message += this.buildReportBackFooter(workflow.metadata.name, targetPhase, phaseFlow);
|
|
148
156
|
if (!skipIncludes) {
|
|
149
157
|
this.assertNoUnresolvedIncludes(message, `${workflow.metadata.name}:${targetPhase} (starting)`);
|
|
@@ -165,13 +173,43 @@ class AIMentor {
|
|
|
165
173
|
status: 'complete'
|
|
166
174
|
};
|
|
167
175
|
}
|
|
168
|
-
const
|
|
176
|
+
const phases = workflow.metadata.phases;
|
|
177
|
+
const phaseFlow = phases?.[phaseId];
|
|
178
|
+
const recurrenceMember = (0, resolve_phase_edge_1.findRecurrenceForMember)(phases, phaseId);
|
|
169
179
|
let nextPhaseId = null;
|
|
170
|
-
if (
|
|
180
|
+
if (recurrenceMember?.role === 'setup') {
|
|
181
|
+
nextPhaseId = resolve_phase_edge_1.WAITING_FOR_NEXT_INVOCATION;
|
|
182
|
+
}
|
|
183
|
+
else if (recurrenceMember?.role === 'recur') {
|
|
184
|
+
const selectedAction = evidence?.selectedReviewAction;
|
|
185
|
+
if (!selectedAction) {
|
|
186
|
+
nextPhaseId = evidence?.reviewHandoff?.reviewRequired === true
|
|
187
|
+
? resolve_phase_edge_1.AWAITING_RECURRING_REVIEW
|
|
188
|
+
: recurrenceMember.node.recur.phase;
|
|
189
|
+
}
|
|
190
|
+
else if (selectedAction.kind === 'request_changes') {
|
|
191
|
+
nextPhaseId = recurrenceMember.node.recur.phase;
|
|
192
|
+
}
|
|
193
|
+
else if (selectedAction.kind === 'approve') {
|
|
194
|
+
const finalInvocation = (0, resolve_phase_edge_1.resolveDiscriminant)(findings, evidence) === 'final-invocation';
|
|
195
|
+
nextPhaseId = finalInvocation
|
|
196
|
+
? (evidence?.assignmentInactive === true
|
|
197
|
+
? recurrenceMember.node.done.phase
|
|
198
|
+
: recurrenceMember.node.recur.phase)
|
|
199
|
+
: resolve_phase_edge_1.WAITING_FOR_NEXT_INVOCATION;
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
nextPhaseId = recurrenceMember.node.recur.phase;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
else if (phaseFlow && !(0, resolve_phase_edge_1.isRecurrencePhase)(phaseFlow) && phaseFlow.onSuccess) {
|
|
171
206
|
// Issue #1123: resolved through the shared authority so the success and
|
|
172
207
|
// failure paths cannot read the discriminant differently.
|
|
173
208
|
nextPhaseId = (0, resolve_phase_edge_1.resolvePhaseEdge)(phaseFlow.onSuccess, (0, resolve_phase_edge_1.resolveDiscriminant)(findings, evidence));
|
|
174
209
|
}
|
|
210
|
+
const nextDefinition = nextPhaseId ? phases?.[nextPhaseId] : undefined;
|
|
211
|
+
if ((0, resolve_phase_edge_1.isRecurrencePhase)(nextDefinition))
|
|
212
|
+
nextPhaseId = nextDefinition.setup.phase;
|
|
175
213
|
let message = '';
|
|
176
214
|
if (nextPhaseId === resolve_phase_edge_1.WAITING_FOR_NEXT_INVOCATION) {
|
|
177
215
|
message += [
|
|
@@ -182,6 +220,13 @@ class AIMentor {
|
|
|
182
220
|
'<!-- FRAIM_RECURRING_PARK -->',
|
|
183
221
|
].join('\n');
|
|
184
222
|
}
|
|
223
|
+
else if (nextPhaseId === resolve_phase_edge_1.AWAITING_RECURRING_REVIEW) {
|
|
224
|
+
message += [
|
|
225
|
+
'### Recurring Result Awaiting Review',
|
|
226
|
+
'',
|
|
227
|
+
'The current recurring result is waiting for manager review. Do not park or advance until an explicit review action is selected.',
|
|
228
|
+
].join('\n');
|
|
229
|
+
}
|
|
185
230
|
else if (nextPhaseId) {
|
|
186
231
|
message += `Moving to the next phase: **${nextPhaseId}**.\n\n`;
|
|
187
232
|
let nextInstructions = workflow.phases.get(nextPhaseId);
|
|
@@ -189,7 +234,10 @@ class AIMentor {
|
|
|
189
234
|
nextInstructions = skipIncludes ? nextInstructions : await this.resolveIncludes(nextInstructions, workflow.path);
|
|
190
235
|
message += nextInstructions;
|
|
191
236
|
}
|
|
192
|
-
const
|
|
237
|
+
const nextRecurrenceMember = (0, resolve_phase_edge_1.findRecurrenceForMember)(phases, nextPhaseId);
|
|
238
|
+
const nextPhaseFlow = nextRecurrenceMember
|
|
239
|
+
? { onSuccess: undefined, onFailure: nextRecurrenceMember.node[nextRecurrenceMember.role].onFailure }
|
|
240
|
+
: ((0, resolve_phase_edge_1.isRecurrencePhase)(workflow.metadata.phases?.[nextPhaseId]) ? undefined : workflow.metadata.phases?.[nextPhaseId]);
|
|
193
241
|
message += this.buildReportBackFooter(workflow.metadata.name, nextPhaseId, nextPhaseFlow);
|
|
194
242
|
}
|
|
195
243
|
else {
|
|
@@ -216,13 +264,15 @@ class AIMentor {
|
|
|
216
264
|
};
|
|
217
265
|
}
|
|
218
266
|
const phaseMeta = workflow.metadata.phases?.[phaseId];
|
|
267
|
+
const recurrenceMember = (0, resolve_phase_edge_1.findRecurrenceForMember)(workflow.metadata.phases, phaseId);
|
|
219
268
|
// Issue #1123: the failure edge may be a discriminant map. Resolving to
|
|
220
269
|
// null (terminal, malformed, or a map with no `default` and no match)
|
|
221
270
|
// falls back to self-retry, which is what the previous `|| phaseId` did
|
|
222
271
|
// for an absent edge. The old expression could not do this: an object is
|
|
223
272
|
// truthy, so it was returned as the target and `phases.get()` then missed.
|
|
224
273
|
const targetPhaseId = status === 'failure'
|
|
225
|
-
? ((0, resolve_phase_edge_1.resolvePhaseEdge)(
|
|
274
|
+
? ((0, resolve_phase_edge_1.resolvePhaseEdge)(recurrenceMember?.node[recurrenceMember.role].onFailure
|
|
275
|
+
?? (!(0, resolve_phase_edge_1.isRecurrencePhase)(phaseMeta) ? phaseMeta?.onFailure : undefined), (0, resolve_phase_edge_1.resolveDiscriminant)(findings, evidence)) || phaseId)
|
|
226
276
|
: phaseId;
|
|
227
277
|
let message = `### Current Phase: ${targetPhaseId}\n\n`;
|
|
228
278
|
let instructions = workflow.phases.get(targetPhaseId);
|
|
@@ -129,6 +129,10 @@ function loadJobPhases(jobId, projectPath, discriminant = 'feature') {
|
|
|
129
129
|
const phaseDef = fm.phases[cursor];
|
|
130
130
|
if (!phaseDef)
|
|
131
131
|
break;
|
|
132
|
+
if ((0, resolve_phase_edge_1.isRecurrencePhase)(phaseDef)) {
|
|
133
|
+
ordered.splice(ordered.length - 1, 1, phaseDef.setup.phase, phaseDef.recur.phase, phaseDef.done.phase);
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
132
136
|
cursor = (0, resolve_phase_edge_1.resolvePhaseEdge)(phaseDef.onSuccess, discriminant);
|
|
133
137
|
}
|
|
134
138
|
const labels = fm.phaseLabels || {};
|
|
@@ -143,7 +147,18 @@ function loadAllJobPhaseIds(jobId, projectPath) {
|
|
|
143
147
|
const phases = loadJobPhasesFromSteps(stubPath);
|
|
144
148
|
return new Set(phases.map((p) => p.id));
|
|
145
149
|
}
|
|
146
|
-
|
|
150
|
+
const ids = new Set();
|
|
151
|
+
for (const [phaseId, phase] of Object.entries(fm.phases)) {
|
|
152
|
+
if ((0, resolve_phase_edge_1.isRecurrencePhase)(phase)) {
|
|
153
|
+
ids.add(phase.setup.phase);
|
|
154
|
+
ids.add(phase.recur.phase);
|
|
155
|
+
ids.add(phase.done.phase);
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
ids.add(phaseId);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return ids;
|
|
147
162
|
}
|
|
148
163
|
function labelForPhaseId(phaseId, jobId, projectPath) {
|
|
149
164
|
const stubPath = findJobStubPath(projectPath, jobId);
|
|
@@ -43,6 +43,8 @@ exports.QUALITY_REGISTRY = {
|
|
|
43
43
|
'ui-quality-assessment': { stage: 'ui-ux-quality', enforced: true, telemetryKind: 'score' },
|
|
44
44
|
// Test Quality
|
|
45
45
|
'test-quality-assessment': { stage: 'test-quality', enforced: true, telemetryKind: 'score' },
|
|
46
|
+
// Performance Quality
|
|
47
|
+
'performance-quality-assessment': { stage: 'performance-quality', enforced: true, telemetryKind: 'score' },
|
|
46
48
|
// Security
|
|
47
49
|
'security-review': { stage: 'security', enforced: true, telemetryKind: 'score' },
|
|
48
50
|
'production-readiness-review': { stage: 'production-readiness', enforced: true, telemetryKind: 'score' },
|
|
@@ -80,6 +82,7 @@ exports.STAGE_DISPLAY_NAMES = {
|
|
|
80
82
|
'product-quality': 'Product Quality',
|
|
81
83
|
'ui-ux-quality': 'UI/UX Quality',
|
|
82
84
|
'test-quality': 'Test Quality',
|
|
85
|
+
'performance-quality': 'Performance Quality',
|
|
83
86
|
'security': 'Security',
|
|
84
87
|
'production-readiness': 'Production Readiness',
|
|
85
88
|
'fundraising': 'Fundraising',
|
|
@@ -95,6 +98,7 @@ exports.ALL_STAGE_CATEGORIES = [
|
|
|
95
98
|
'product-quality',
|
|
96
99
|
'ui-ux-quality',
|
|
97
100
|
'test-quality',
|
|
101
|
+
'performance-quality',
|
|
98
102
|
'security',
|
|
99
103
|
'production-readiness',
|
|
100
104
|
'fundraising',
|
|
@@ -169,6 +173,12 @@ const QUALITY_SCORE_DIMENSIONS = {
|
|
|
169
173
|
'testDesign',
|
|
170
174
|
'reliability'
|
|
171
175
|
],
|
|
176
|
+
'performance-quality-assessment': [
|
|
177
|
+
'memoryEfficiency',
|
|
178
|
+
'cpuEfficiency',
|
|
179
|
+
'diskIO',
|
|
180
|
+
'startupCost'
|
|
181
|
+
],
|
|
172
182
|
'security-review': [
|
|
173
183
|
'findingSeverity',
|
|
174
184
|
'remediationReadiness',
|
|
@@ -15,15 +15,33 @@
|
|
|
15
15
|
* change inert for every job that authors no map.
|
|
16
16
|
*/
|
|
17
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
-
exports.FEEDBACK_PHASE_ID = exports.WAITING_FOR_NEXT_INVOCATION = void 0;
|
|
18
|
+
exports.FEEDBACK_PHASE_ID = exports.AWAITING_RECURRING_REVIEW = exports.WAITING_FOR_NEXT_INVOCATION = void 0;
|
|
19
|
+
exports.isRecurrencePhase = isRecurrencePhase;
|
|
20
|
+
exports.findRecurrenceForMember = findRecurrenceForMember;
|
|
19
21
|
exports.resolvePhaseEdge = resolvePhaseEdge;
|
|
20
22
|
exports.resolveDiscriminant = resolveDiscriminant;
|
|
21
23
|
exports.discriminantKeys = discriminantKeys;
|
|
22
24
|
exports.derivePredecessorPhase = derivePredecessorPhase;
|
|
25
|
+
function isRecurrencePhase(phase) {
|
|
26
|
+
return !!phase && 'type' in phase && phase.type === 'recurrence';
|
|
27
|
+
}
|
|
28
|
+
function findRecurrenceForMember(phases, phaseId) {
|
|
29
|
+
for (const [nodeId, phase] of Object.entries(phases ?? {})) {
|
|
30
|
+
if (!isRecurrencePhase(phase))
|
|
31
|
+
continue;
|
|
32
|
+
for (const role of ['setup', 'recur', 'done']) {
|
|
33
|
+
if (phase[role].phase === phaseId)
|
|
34
|
+
return { nodeId, node: phase, role };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
23
39
|
/** The default discriminant, and the mandatory key on every authored map. */
|
|
24
40
|
const DEFAULT_DISCRIMINANT = 'default';
|
|
25
41
|
/** Reserved transition target that parks a recurring job until the scheduler invokes it again. */
|
|
26
42
|
exports.WAITING_FOR_NEXT_INVOCATION = 'waiting-for-next-invocation';
|
|
43
|
+
/** Reserved controller target while a recurring result awaits manager review. */
|
|
44
|
+
exports.AWAITING_RECURRING_REVIEW = 'awaiting-recurring-review';
|
|
27
45
|
/**
|
|
28
46
|
* Resolve an edge to the next phase id.
|
|
29
47
|
*
|
|
@@ -97,7 +115,7 @@ exports.FEEDBACK_PHASE_ID = 'address-feedback';
|
|
|
97
115
|
function derivePredecessorPhase(phases, targetPhaseId) {
|
|
98
116
|
const predecessors = [];
|
|
99
117
|
for (const [phaseId, edges] of Object.entries(phases ?? {})) {
|
|
100
|
-
if (phaseId === targetPhaseId || !edges)
|
|
118
|
+
if (phaseId === targetPhaseId || !edges || isRecurrencePhase(edges))
|
|
101
119
|
continue;
|
|
102
120
|
const edge = edges.onSuccess;
|
|
103
121
|
const targets = typeof edge === 'string'
|
|
@@ -135,13 +135,19 @@ function persistShellPath(includePackagedFraimBin = false) {
|
|
|
135
135
|
// Versioned dir first (has fresh installs, matches getManagedAgentBinDirs()
|
|
136
136
|
// order), flat dir second (retains bare POSIX agent scripts as fallback).
|
|
137
137
|
exportLine = portableNodeBin !== nodeRoot
|
|
138
|
-
? `export PATH
|
|
139
|
-
: `export PATH
|
|
138
|
+
? `export PATH="$PATH:$HOME/.fraim/node/${path_1.default.basename(portableNodeBin)}:$HOME/.fraim/node${fraimBinEntry}"`
|
|
139
|
+
: `export PATH="$PATH:$HOME/.fraim/node${fraimBinEntry}"`;
|
|
140
140
|
}
|
|
141
141
|
else {
|
|
142
142
|
exportLine = includePackagedFraimBin
|
|
143
|
-
? 'export PATH
|
|
144
|
-
: 'export PATH
|
|
143
|
+
? 'export PATH="$PATH:$HOME/.fraim/bin:$HOME/.fraim/node/bin"'
|
|
144
|
+
: 'export PATH="$PATH:$HOME/.fraim/node/bin"';
|
|
145
|
+
}
|
|
146
|
+
// Guard against editor-autocorrect curly-quote regression (issue #1688):
|
|
147
|
+
// U+201C/201D/2018/2019 in exportLine would be written verbatim to shell
|
|
148
|
+
// profiles and break PATH resolution for all pre-existing PATH entries.
|
|
149
|
+
if (/[“”‘’]/.test(exportLine)) {
|
|
150
|
+
throw new Error(`persistShellPath: exportLine contains curly quotes. Writing this to a shell profile would corrupt PATH resolution. Check for editor autocorrect in session-service.ts. Line: ${JSON.stringify(exportLine)}`);
|
|
145
151
|
}
|
|
146
152
|
const stanza = `\n${marker}\n${exportLine}\n`;
|
|
147
153
|
if (process.platform === 'win32') {
|
|
@@ -149,7 +155,16 @@ function persistShellPath(includePackagedFraimBin = false) {
|
|
|
149
155
|
? [path_1.default.join((0, script_sync_utils_1.getUserFraimDir)(), 'bin'), ...(0, managed_agent_paths_1.getManagedAgentBinDirs)()]
|
|
150
156
|
: (0, managed_agent_paths_1.getManagedAgentBinDirs)();
|
|
151
157
|
const psCmd = buildPersistShellPathWindowsCommand(bins);
|
|
152
|
-
|
|
158
|
+
// Issue #1692: a sandboxed test run (row detection on load, node row
|
|
159
|
+
// success, fraim row success all reach this function) must never write
|
|
160
|
+
// its temp fake-home bin dirs into the real, registry-backed Windows
|
|
161
|
+
// User PATH. FRAIM_TEST_SANDBOX is already the canonical "this process
|
|
162
|
+
// is a sandboxed test run" signal set by every test runner (see
|
|
163
|
+
// getUserFraimDirPath() in project-fraim-paths.ts) rather than a
|
|
164
|
+
// dedicated flag for this call site.
|
|
165
|
+
if (process.env.FRAIM_TEST_SANDBOX !== '1') {
|
|
166
|
+
(0, child_process_1.spawnSync)('powershell', ['-NoProfile', '-NonInteractive', '-Command', psCmd], { encoding: 'utf8' });
|
|
167
|
+
}
|
|
153
168
|
// Fall through to write POSIX profile files for Git Bash (VS Code
|
|
154
169
|
// integrated terminal, Claude Code, etc.) which do not inherit the
|
|
155
170
|
// Windows user PATH written above.
|
|
@@ -190,7 +205,7 @@ function persistShellPath(includePackagedFraimBin = false) {
|
|
|
190
205
|
if (!content.includes(exportLine)) {
|
|
191
206
|
// Stale export line: versioned dir changed, includePackagedFraimBin
|
|
192
207
|
// toggled, or platform-specific line was never written. Replace it.
|
|
193
|
-
fs_1.default.writeFileSync(profile, content.replace(/export PATH
|
|
208
|
+
fs_1.default.writeFileSync(profile, content.replace(/export PATH=[\u201D"][^\u201D"]*\.fraim[^\u201D"]*[\u201D"]/, exportLine), 'utf8');
|
|
194
209
|
}
|
|
195
210
|
continue;
|
|
196
211
|
}
|
|
@@ -545,11 +545,14 @@ function collectPendingL0SourceFiles(workspaceRoot, resolvedUserId, roots) {
|
|
|
545
545
|
* it cannot drift from the read path again.
|
|
546
546
|
*/
|
|
547
547
|
function orgCacheDisplayPath(fileName) {
|
|
548
|
-
const fraimDir = (0, project_fraim_paths_1.getUserFraimDirPath)();
|
|
549
548
|
const cachePath = (0, path_1.join)((0, pack_home_1.resolvePackHome)('org').contentRoot, 'learnings', fileName);
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
549
|
+
return userFraimDisplayPathFor(cachePath);
|
|
550
|
+
}
|
|
551
|
+
function userFraimDisplayPathFor(absolutePath) {
|
|
552
|
+
const fraimDir = (0, project_fraim_paths_1.getUserFraimDirPath)();
|
|
553
|
+
const rel = absolutePath.startsWith(fraimDir)
|
|
554
|
+
? absolutePath.slice(fraimDir.length).replace(/\\/g, '/').replace(/^\//, '')
|
|
555
|
+
: absolutePath.replace(/\\/g, '/');
|
|
553
556
|
return (0, project_fraim_paths_1.getUserFraimDisplayPath)(rel);
|
|
554
557
|
}
|
|
555
558
|
/**
|
|
@@ -944,7 +947,7 @@ function resolveOrgContextFile(workspaceRoot, relativePath, orgCacheEligible = t
|
|
|
944
947
|
if ((0, fs_1.existsSync)(cachePath)) {
|
|
945
948
|
return {
|
|
946
949
|
present: true,
|
|
947
|
-
displayPath: (
|
|
950
|
+
displayPath: userFraimDisplayPathFor(cachePath)
|
|
948
951
|
};
|
|
949
952
|
}
|
|
950
953
|
}
|
|
@@ -46,13 +46,37 @@ function buildEntryIndex(workspaceRoot, userId) {
|
|
|
46
46
|
continue;
|
|
47
47
|
}
|
|
48
48
|
for (const entry of entries) {
|
|
49
|
+
const fileType = fileTypeOf(entry);
|
|
50
|
+
const attestable = {
|
|
51
|
+
key: entry.id ?? (0, learning_usage_store_1.titleUsageKey)(fileType, entry.title),
|
|
52
|
+
family: 'learning',
|
|
53
|
+
title: entry.title,
|
|
54
|
+
fileType,
|
|
55
|
+
level: entry.level,
|
|
56
|
+
};
|
|
49
57
|
if (entry.id && !byId.has(entry.id))
|
|
50
|
-
byId.set(entry.id,
|
|
51
|
-
const
|
|
52
|
-
if (!byTitle.has(
|
|
53
|
-
byTitle.set(
|
|
58
|
+
byId.set(entry.id, attestable);
|
|
59
|
+
const titleKey = (0, learning_usage_store_1.normalizeEntryTitle)(entry.title);
|
|
60
|
+
if (!byTitle.has(titleKey))
|
|
61
|
+
byTitle.set(titleKey, attestable);
|
|
54
62
|
}
|
|
55
63
|
}
|
|
64
|
+
for (const rule of (0, learning_context_builder_1.collectOfferedRuleFiles)(workspaceRoot, true)) {
|
|
65
|
+
const attestable = {
|
|
66
|
+
key: rule.key,
|
|
67
|
+
family: 'rule',
|
|
68
|
+
title: rule.title,
|
|
69
|
+
fileType: rule.fileType,
|
|
70
|
+
level: rule.level,
|
|
71
|
+
};
|
|
72
|
+
if (!byId.has(rule.key))
|
|
73
|
+
byId.set(rule.key, attestable);
|
|
74
|
+
const titleKey = (0, learning_usage_store_1.normalizeEntryTitle)(rule.title);
|
|
75
|
+
if (!byTitle.has(titleKey))
|
|
76
|
+
byTitle.set(titleKey, attestable);
|
|
77
|
+
if (rule.id && !byId.has(rule.id))
|
|
78
|
+
byId.set(rule.id, attestable);
|
|
79
|
+
}
|
|
56
80
|
return { byId, byTitle };
|
|
57
81
|
}
|
|
58
82
|
function fileTypeOf(entry) {
|
|
@@ -99,12 +123,11 @@ function recordAttestationsFromRetrospective(workspaceRoot, userId, options) {
|
|
|
99
123
|
continue;
|
|
100
124
|
}
|
|
101
125
|
}
|
|
102
|
-
const fileType = fileTypeOf(entry);
|
|
103
126
|
inputs.push({
|
|
104
|
-
key: entry.
|
|
105
|
-
family:
|
|
127
|
+
key: entry.key,
|
|
128
|
+
family: entry.family,
|
|
106
129
|
title: entry.title,
|
|
107
|
-
fileType,
|
|
130
|
+
fileType: entry.fileType,
|
|
108
131
|
level: entry.level,
|
|
109
132
|
outcome: item.outcome,
|
|
110
133
|
note: item.note,
|
|
@@ -347,7 +347,7 @@ Do not use this tool for other repositories or external project issue trackers.`
|
|
|
347
347
|
},
|
|
348
348
|
status: {
|
|
349
349
|
type: 'string',
|
|
350
|
-
description: 'Status of your work in the current phase',
|
|
350
|
+
description: 'Status of your work in the current phase.',
|
|
351
351
|
enum: ['starting', 'complete', 'incomplete', 'failure']
|
|
352
352
|
},
|
|
353
353
|
findings: {
|
|
@@ -106,6 +106,7 @@ async function seedDemoDataForUser(dbService, userId) {
|
|
|
106
106
|
{ category: 'product-quality', jobName: 'codebase-quality-assessment' },
|
|
107
107
|
{ category: 'ui-ux-quality', jobName: 'ui-quality-assessment' },
|
|
108
108
|
{ category: 'test-quality', jobName: 'test-quality-assessment' },
|
|
109
|
+
{ category: 'performance-quality', jobName: 'performance-quality-assessment' },
|
|
109
110
|
{ category: 'security', jobName: 'security-review' },
|
|
110
111
|
{ category: 'production-readiness', jobName: 'production-readiness-review' },
|
|
111
112
|
{ category: 'fundraising', jobName: 'fundraising-evidence-review' },
|