fraim 2.0.280 → 2.0.283

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.
Files changed (31) hide show
  1. package/README.md +467 -467
  2. package/bin/fraim.js +12 -12
  3. package/dist/src/cli/mcp/fraim-mcp-latest-launcher.js +266 -182
  4. package/dist/src/cli/mcp/mcp-server-registry.js +11 -3
  5. package/dist/src/cli/setup/ide-invocation-surfaces.js +64 -64
  6. package/dist/src/cli/utils/agent-adapters.js +61 -61
  7. package/dist/src/core/ai-mentor.js +35 -0
  8. package/dist/src/core/handoff-contracts.js +73 -46
  9. package/dist/src/core/resolve-phase-edge.js +33 -0
  10. package/dist/src/core/utils/stub-generator.js +53 -53
  11. package/dist/src/first-run/server.js +5 -1
  12. package/dist/src/first-run/session-service.js +48 -12
  13. package/dist/src/fraim/issues.js +4 -4
  14. package/dist/src/local-mcp-server/stdio-server.js +43 -4
  15. package/dist/src/mcp/tool-schemas.js +40 -40
  16. package/dist/src/middleware/telemetry.js +21 -21
  17. package/dist/src/services/email-service.js +623 -623
  18. package/dist/src/services/installer-service.js +22 -22
  19. package/index.js +83 -83
  20. package/package.json +59 -59
  21. package/public/first-run/error-frame.js +100 -100
  22. package/public/first-run/index.html +35 -35
  23. package/public/first-run/script.js +747 -742
  24. package/public/first-run/styles.css +929 -929
  25. package/dist/src/cli/commands/learning-usage.js +0 -412
  26. package/dist/src/cli/commands/test-mcp.js +0 -171
  27. package/dist/src/cli/setup/first-run.js +0 -242
  28. package/dist/src/core/config-writer.js +0 -75
  29. package/dist/src/core/utils/job-aliases.js +0 -47
  30. package/dist/src/core/utils/workflow-parser.js +0 -174
  31. package/dist/src/services/email-service-clean.js +0 -782
@@ -14,9 +14,9 @@ exports.buildAntigravityCommandContent = buildAntigravityCommandContent;
14
14
  exports.buildGeminiCommandContent = buildGeminiCommandContent;
15
15
  exports.describeInvocationSurface = describeInvocationSurface;
16
16
  exports.FRAIM_LAUNCH_PHRASE = 'Use FRAIM for <job or task>';
17
- exports.CURSOR_MDC_FRONTMATTER = `---
18
- description: FRAIM discovery and execution contract
19
- alwaysApply: true
17
+ exports.CURSOR_MDC_FRONTMATTER = `---
18
+ description: FRAIM discovery and execution contract
19
+ alwaysApply: true
20
20
  ---`;
21
21
  exports.FRAIM_DEFERRED_TOOL_PRELOAD = [
22
22
  'fraim_connect',
@@ -38,112 +38,112 @@ function buildDeferredToolBootstrapSection(profile) {
38
38
  const preloadList = exports.FRAIM_DEFERRED_TOOL_PRELOAD.map((toolName) => `\`${toolName}\``).join(', ');
39
39
  switch (profile) {
40
40
  case 'claude-toolsearch':
41
- return `0. **Preload deferred FRAIM tools when needed**:
42
- - If FRAIM MCP tools are unavailable because this host lazily loads deferred tool schemas, call \`ToolSearch\` once to load ${preloadList}.
43
- - Do the preload as one batched discovery step, not one search per tool.
44
-
41
+ return `0. **Preload deferred FRAIM tools when needed**:
42
+ - If FRAIM MCP tools are unavailable because this host lazily loads deferred tool schemas, call \`ToolSearch\` once to load ${preloadList}.
43
+ - Do the preload as one batched discovery step, not one search per tool.
44
+
45
45
  `;
46
46
  case 'codex-tool-search':
47
- return `0. **Preload deferred FRAIM tools when needed**:
48
- - If FRAIM MCP tools are unavailable because this host lazily loads deferred tool schemas, call \`tool_search\` once to load ${preloadList}.
49
- - Do the preload as one batched discovery step, not one search per tool.
50
-
47
+ return `0. **Preload deferred FRAIM tools when needed**:
48
+ - If FRAIM MCP tools are unavailable because this host lazily loads deferred tool schemas, call \`tool_search\` once to load ${preloadList}.
49
+ - Do the preload as one batched discovery step, not one search per tool.
50
+
51
51
  `;
52
52
  case 'generic-tool-discovery':
53
- return `0. **Preload deferred FRAIM tools when needed**:
54
- - If FRAIM MCP tools are unavailable because this host lazily loads deferred tool schemas, use the host's tool discovery surface once to load ${preloadList}.
55
- - Do the preload as one batched discovery step, not one search per tool.
56
-
53
+ return `0. **Preload deferred FRAIM tools when needed**:
54
+ - If FRAIM MCP tools are unavailable because this host lazily loads deferred tool schemas, use the host's tool discovery surface once to load ${preloadList}.
55
+ - Do the preload as one batched discovery step, not one search per tool.
56
+
57
57
  `;
58
58
  default:
59
59
  return '';
60
60
  }
61
61
  }
62
62
  function buildFraimInvocationBody(profile = 'none') {
63
- return `Follow this process:
64
-
65
- ${buildDeferredToolBootstrapSection(profile)}1. **Confirm FRAIM activation**:
66
- Use this process only when the user explicitly invokes FRAIM, names a FRAIM job, asks what FRAIM job to run, or the active surface has already selected a FRAIM job. For ordinary requests, answer or work normally; do not scan FRAIM stubs first.
67
-
68
- 2. **If the user did not specify a FRAIM job or topic after activation**:
69
- If local FRAIM job stubs are present in the workspace, inspect those first and match the request locally. Also inspect \`fraim/personalized-employee/jobs/\` for local overrides or repo-specific jobs. If local files are missing or you cannot inspect workspace files, call \`list_fraim_jobs()\` to view the full catalog, including any proxy-discoverable personalized jobs.
70
-
71
- 3. **Find the match**:
72
- If the user names an exact FRAIM job, call \`get_fraim_job({ job: "<job-name>" })\` directly. Otherwise, match the user's request to a FRAIM job from the local stub catalog, \`fraim/personalized-employee/jobs/\`, or the full \`list_fraim_jobs()\` response. If no exact or high-confidence job match exists, say that no FRAIM job matches and continue with normal tools or ask one concise clarification. Do not pick the nearest catalog job.
73
-
74
- 4. **Load the full content**:
75
- - For jobs, call \`get_fraim_job({ job: "<matched-job-name>" })\`.
76
- - For skills, use the content returned by \`get_fraim_file(...)\`.
77
-
78
- 5. **Execute**:
79
- - For jobs, follow the phased instructions and use \`seekMentoring\` when the job requires phase transitions.
80
- - For skills, apply the skill steps directly to the user's current context.
81
- - ${exports.FRAIM_MCP_UNAVAILABLE_MANAGER_GUIDANCE}
63
+ return `Follow this process:
64
+
65
+ ${buildDeferredToolBootstrapSection(profile)}1. **Confirm FRAIM activation**:
66
+ Use this process only when the user explicitly invokes FRAIM, names a FRAIM job, asks what FRAIM job to run, or the active surface has already selected a FRAIM job. For ordinary requests, answer or work normally; do not scan FRAIM stubs first.
67
+
68
+ 2. **If the user did not specify a FRAIM job or topic after activation**:
69
+ If local FRAIM job stubs are present in the workspace, inspect those first and match the request locally. Also inspect \`fraim/personalized-employee/jobs/\` for local overrides or repo-specific jobs. If local files are missing or you cannot inspect workspace files, call \`list_fraim_jobs()\` to view the full catalog, including any proxy-discoverable personalized jobs.
70
+
71
+ 3. **Find the match**:
72
+ If the user names an exact FRAIM job, call \`get_fraim_job({ job: "<job-name>" })\` directly. Otherwise, match the user's request to a FRAIM job from the local stub catalog, \`fraim/personalized-employee/jobs/\`, or the full \`list_fraim_jobs()\` response. If no exact or high-confidence job match exists, say that no FRAIM job matches and continue with normal tools or ask one concise clarification. Do not pick the nearest catalog job.
73
+
74
+ 4. **Load the full content**:
75
+ - For jobs, call \`get_fraim_job({ job: "<matched-job-name>" })\`.
76
+ - For skills, use the content returned by \`get_fraim_file(...)\`.
77
+
78
+ 5. **Execute**:
79
+ - For jobs, follow the phased instructions and use \`seekMentoring\` when the job requires phase transitions.
80
+ - For skills, apply the skill steps directly to the user's current context.
81
+ - ${exports.FRAIM_MCP_UNAVAILABLE_MANAGER_GUIDANCE}
82
82
  `;
83
83
  }
84
84
  exports.FRAIM_INVOCATION_BODY = buildFraimInvocationBody();
85
85
  function buildClaudeSkillContent() {
86
- return `# FRAIM
87
-
86
+ return `# FRAIM
87
+
88
88
  ${buildFraimInvocationBody('claude-toolsearch')}`;
89
89
  }
90
90
  function buildClaudeCommandShimContent() {
91
- return `# FRAIM Compatibility Command
92
-
93
- Use the FRAIM skill when Claude exposes skills directly. This compatibility command keeps \`/fraim\` working on surfaces that still discover legacy command files.
94
-
91
+ return `# FRAIM Compatibility Command
92
+
93
+ Use the FRAIM skill when Claude exposes skills directly. This compatibility command keeps \`/fraim\` working on surfaces that still discover legacy command files.
94
+
95
95
  ${buildFraimInvocationBody('claude-toolsearch')}`;
96
96
  }
97
97
  function buildClaudeSlashCommandContent() {
98
98
  return buildClaudeCommandShimContent();
99
99
  }
100
100
  function buildCursorMentionRuleContent() {
101
- return `${exports.CURSOR_MDC_FRONTMATTER}
102
-
103
- # FRAIM
104
-
105
- ${buildFraimInvocationBody('generic-tool-discovery')}
101
+ return `${exports.CURSOR_MDC_FRONTMATTER}
102
+
103
+ # FRAIM
104
+
105
+ ${buildFraimInvocationBody('generic-tool-discovery')}
106
106
  `;
107
107
  }
108
- exports.CODEX_SKILL_FRONTMATTER = `---
109
- name: fraim
110
- description: Discover and execute FRAIM jobs and skills from Codex.
108
+ exports.CODEX_SKILL_FRONTMATTER = `---
109
+ name: fraim
110
+ description: Discover and execute FRAIM jobs and skills from Codex.
111
111
  ---`;
112
112
  function buildCodexSkillContent() {
113
- return `${exports.CODEX_SKILL_FRONTMATTER}
114
- # FRAIM
115
-
113
+ return `${exports.CODEX_SKILL_FRONTMATTER}
114
+ # FRAIM
115
+
116
116
  ${buildFraimInvocationBody('codex-tool-search')}`;
117
117
  }
118
118
  function buildGrokSkillContent() {
119
- return `# FRAIM
120
-
119
+ return `# FRAIM
120
+
121
121
  ${buildFraimInvocationBody('generic-tool-discovery')}`;
122
122
  }
123
123
  function buildWindsurfCommandContent() {
124
- return `# FRAIM
125
-
124
+ return `# FRAIM
125
+
126
126
  ${buildFraimInvocationBody('generic-tool-discovery')}`;
127
127
  }
128
128
  function buildKiroCommandContent() {
129
- return `# FRAIM
130
-
129
+ return `# FRAIM
130
+
131
131
  ${buildFraimInvocationBody('generic-tool-discovery')}`;
132
132
  }
133
133
  function escapeTomlMultiline(value) {
134
134
  return value.replace(/"""/g, '\\"""');
135
135
  }
136
136
  function buildAntigravityCommandContent() {
137
- return `# FRAIM
138
-
137
+ return `# FRAIM
138
+
139
139
  ${buildFraimInvocationBody('generic-tool-discovery')}`;
140
140
  }
141
141
  function buildGeminiCommandContent() {
142
- return `description = "Discover and execute FRAIM jobs and skills"
143
- prompt = """
144
- # FRAIM
145
-
146
- ${escapeTomlMultiline(buildFraimInvocationBody('generic-tool-discovery'))}
142
+ return `description = "Discover and execute FRAIM jobs and skills"
143
+ prompt = """
144
+ # FRAIM
145
+
146
+ ${escapeTomlMultiline(buildFraimInvocationBody('generic-tool-discovery'))}
147
147
  """`;
148
148
  }
149
149
  function describeInvocationSurface(ideName, invocationProfile) {
@@ -25,9 +25,9 @@ function adapterConfigTypes(file) {
25
25
  return Array.isArray(file.configType) ? file.configType : [file.configType];
26
26
  }
27
27
  function buildManagedSection(body) {
28
- return `${START_MARKER}
29
- ${body.trim()}
30
- ${END_MARKER}
28
+ return `${START_MARKER}
29
+ ${body.trim()}
30
+ ${END_MARKER}
31
31
  `;
32
32
  }
33
33
  function mergeManagedSection(existingContent, managedSection) {
@@ -59,69 +59,69 @@ function getAdapterFiles(allowedConfigTypes = null) {
59
59
  const employeeRulesPath = (0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)('ai-employee/rules');
60
60
  const personalizedRootPath = (0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)('personalized-employee');
61
61
  const projectRulesPath = (0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)('personalized-employee/rules/project_rules.md');
62
- const markdownBody = buildManagedSection(`
63
- ## FRAIM
64
-
65
- This repository uses FRAIM.
66
-
67
- - The FRAIM discovery catalog lives under \`${fraimRoot}/\`.
68
- - Jobs under \`${employeeJobsPath}/\` and \`${managerJobsPath}/\` are FRAIM's primary execution units. Treat them like first-class workflows when deciding how to execute work.
69
- - Skills under \`${employeeSkillsPath}/\` are reusable capabilities that jobs compose.
70
- - Rules under \`${employeeRulesPath}/\` are always-on constraints and conventions.
71
- - Repo-specific overrides and learning artifacts live under \`${personalizedRootPath}/\` and take precedence over synced baseline content.
72
- - Use FRAIM when the user explicitly invokes FRAIM, names a FRAIM job, asks what FRAIM job to run, or the active surface has already selected a FRAIM job.
73
- - For ordinary requests, answer or work normally. Do not scan FRAIM stubs first.
74
- - If the user names an exact FRAIM job, call \`get_fraim_job({ job: "<job-name>" })\` directly.
75
- - When FRAIM routing is active but the job is not exact, scan the job stubs under \`${employeeJobsPath}/\` and \`${managerJobsPath}/\` to identify the most appropriate job. Read stub filenames and their Intent/Outcome sections before using catalog tools.
76
- - Once you identify the relevant job, call \`get_fraim_job({ job: "<job-name>" })\` to get the full phased instructions.
77
- - If no exact or high-confidence job match exists, say that no FRAIM job matches and continue with normal tools or ask one concise clarification.
78
- - For deeper capability detail, call \`get_fraim_file({ path: "skills/<category>/<skill-name>.md" })\` or \`get_fraim_file({ path: "rules/<category>/<rule-name>.md" })\`.
79
- - Read \`${projectRulesPath}\` if it exists before doing work.
80
- - When users ask for next step recommendations, use recommend-next-job skill under \`${employeeSkillsPath}/\` to gather context before suggesting jobs.
81
- - ${ide_invocation_surfaces_1.FRAIM_MCP_UNAVAILABLE_MANAGER_GUIDANCE}
82
-
83
- > [!IMPORTANT]
84
- > **Job stubs are for discovery only.** When a user mentions or references any file under \`${employeeJobsPath}/\` or \`${managerJobsPath}/\`, do NOT attempt to execute the job from the stub content. The stub only shows intent and phase names. Always call \`get_fraim_job({ job: "<job-name>" })\` first to get the full phased instructions before doing any work.
62
+ const markdownBody = buildManagedSection(`
63
+ ## FRAIM
64
+
65
+ This repository uses FRAIM.
66
+
67
+ - The FRAIM discovery catalog lives under \`${fraimRoot}/\`.
68
+ - Jobs under \`${employeeJobsPath}/\` and \`${managerJobsPath}/\` are FRAIM's primary execution units. Treat them like first-class workflows when deciding how to execute work.
69
+ - Skills under \`${employeeSkillsPath}/\` are reusable capabilities that jobs compose.
70
+ - Rules under \`${employeeRulesPath}/\` are always-on constraints and conventions.
71
+ - Repo-specific overrides and learning artifacts live under \`${personalizedRootPath}/\` and take precedence over synced baseline content.
72
+ - Use FRAIM when the user explicitly invokes FRAIM, names a FRAIM job, asks what FRAIM job to run, or the active surface has already selected a FRAIM job.
73
+ - For ordinary requests, answer or work normally. Do not scan FRAIM stubs first.
74
+ - If the user names an exact FRAIM job, call \`get_fraim_job({ job: "<job-name>" })\` directly.
75
+ - When FRAIM routing is active but the job is not exact, scan the job stubs under \`${employeeJobsPath}/\` and \`${managerJobsPath}/\` to identify the most appropriate job. Read stub filenames and their Intent/Outcome sections before using catalog tools.
76
+ - Once you identify the relevant job, call \`get_fraim_job({ job: "<job-name>" })\` to get the full phased instructions.
77
+ - If no exact or high-confidence job match exists, say that no FRAIM job matches and continue with normal tools or ask one concise clarification.
78
+ - For deeper capability detail, call \`get_fraim_file({ path: "skills/<category>/<skill-name>.md" })\` or \`get_fraim_file({ path: "rules/<category>/<rule-name>.md" })\`.
79
+ - Read \`${projectRulesPath}\` if it exists before doing work.
80
+ - When users ask for next step recommendations, use recommend-next-job skill under \`${employeeSkillsPath}/\` to gather context before suggesting jobs.
81
+ - ${ide_invocation_surfaces_1.FRAIM_MCP_UNAVAILABLE_MANAGER_GUIDANCE}
82
+
83
+ > [!IMPORTANT]
84
+ > **Job stubs are for discovery only.** When a user mentions or references any file under \`${employeeJobsPath}/\` or \`${managerJobsPath}/\`, do NOT attempt to execute the job from the stub content. The stub only shows intent and phase names. Always call \`get_fraim_job({ job: "<job-name>" })\` first to get the full phased instructions before doing any work.
85
85
  `);
86
- const cursorManagedBody = buildManagedSection(`
87
- # FRAIM
88
-
89
- ${(0, ide_invocation_surfaces_1.buildFraimInvocationBody)('generic-tool-discovery')}
86
+ const cursorManagedBody = buildManagedSection(`
87
+ # FRAIM
88
+
89
+ ${(0, ide_invocation_surfaces_1.buildFraimInvocationBody)('generic-tool-discovery')}
90
90
  `);
91
- const copilotBody = buildManagedSection(`
92
- ## FRAIM
93
-
94
- - Use \`${fraimRoot}/\` as the repository's FRAIM catalog.
95
- - FRAIM jobs are the primary execution units and should be treated like first-class workflows.
96
- - FRAIM skills are reusable capabilities jobs compose.
97
- - FRAIM rules are always-on constraints and conventions.
98
- - Repo-specific overrides and learnings live under \`${personalizedRootPath}/\`.
99
- - Use FRAIM when the user explicitly invokes FRAIM, names a FRAIM job, asks what FRAIM job to run, or the active surface has already selected a FRAIM job.
100
- - For ordinary requests, answer or work normally. Do not scan FRAIM stubs first.
101
- - If the user names an exact FRAIM job, fetch that full job directly with FRAIM MCP tools.
102
- - When FRAIM routing is active but the job is not exact, use local stubs to identify which job to invoke before fetching full content with FRAIM MCP tools.
103
- - If no exact or high-confidence job match exists, say that no FRAIM job matches and continue with normal tools or ask one concise clarification.
104
- - **Job stubs are for discovery only.** Never execute a job from stub content - always call \`get_fraim_job({ job: "<job-name>" })\` first.
105
- - ${ide_invocation_surfaces_1.FRAIM_MCP_UNAVAILABLE_MANAGER_GUIDANCE}
91
+ const copilotBody = buildManagedSection(`
92
+ ## FRAIM
93
+
94
+ - Use \`${fraimRoot}/\` as the repository's FRAIM catalog.
95
+ - FRAIM jobs are the primary execution units and should be treated like first-class workflows.
96
+ - FRAIM skills are reusable capabilities jobs compose.
97
+ - FRAIM rules are always-on constraints and conventions.
98
+ - Repo-specific overrides and learnings live under \`${personalizedRootPath}/\`.
99
+ - Use FRAIM when the user explicitly invokes FRAIM, names a FRAIM job, asks what FRAIM job to run, or the active surface has already selected a FRAIM job.
100
+ - For ordinary requests, answer or work normally. Do not scan FRAIM stubs first.
101
+ - If the user names an exact FRAIM job, fetch that full job directly with FRAIM MCP tools.
102
+ - When FRAIM routing is active but the job is not exact, use local stubs to identify which job to invoke before fetching full content with FRAIM MCP tools.
103
+ - If no exact or high-confidence job match exists, say that no FRAIM job matches and continue with normal tools or ask one concise clarification.
104
+ - **Job stubs are for discovery only.** Never execute a job from stub content - always call \`get_fraim_job({ job: "<job-name>" })\` first.
105
+ - ${ide_invocation_surfaces_1.FRAIM_MCP_UNAVAILABLE_MANAGER_GUIDANCE}
106
106
  `);
107
- const fraimReadme = `# FRAIM Catalog
108
-
109
- This directory is the repository-visible FRAIM surface.
110
-
111
- - \`ai-employee/jobs/\`: employee job stubs
112
- - \`ai-manager/jobs/\`: manager job stubs
113
- - \`ai-employee/skills/\`: skill stubs
114
- - \`ai-employee/rules/\`: rule stubs
115
- - \`personalized-employee/\`: repo-specific overrides and learnings
116
-
117
- When FRAIM routing is active and no exact FRAIM job is named, use the stubs here to discover which FRAIM job, skill, or rule is relevant, then load the full content through FRAIM MCP tools. If an exact FRAIM job is named, load it directly. For ordinary requests, do not scan this catalog first.
107
+ const fraimReadme = `# FRAIM Catalog
108
+
109
+ This directory is the repository-visible FRAIM surface.
110
+
111
+ - \`ai-employee/jobs/\`: employee job stubs
112
+ - \`ai-manager/jobs/\`: manager job stubs
113
+ - \`ai-employee/skills/\`: skill stubs
114
+ - \`ai-employee/rules/\`: rule stubs
115
+ - \`personalized-employee/\`: repo-specific overrides and learnings
116
+
117
+ When FRAIM routing is active and no exact FRAIM job is named, use the stubs here to discover which FRAIM job, skill, or rule is relevant, then load the full content through FRAIM MCP tools. If an exact FRAIM job is named, load it directly. For ordinary requests, do not scan this catalog first.
118
118
  `;
119
- const vscodePrompt = `# FRAIM
120
-
119
+ const vscodePrompt = `# FRAIM
120
+
121
121
  ${(0, ide_invocation_surfaces_1.buildFraimInvocationBody)('generic-tool-discovery')}`;
122
- const geminiProjectInstructions = `# Gemini Project Instructions
123
-
124
- @../AGENTS.md
122
+ const geminiProjectInstructions = `# Gemini Project Instructions
123
+
124
+ @../AGENTS.md
125
125
  `;
126
126
  const all = [
127
127
  { path: 'AGENTS.md', content: markdownBody, configType: 'standard' },
@@ -230,6 +230,41 @@ class AIMentor {
230
230
  status
231
231
  };
232
232
  }
233
+ /**
234
+ * The job's declared phase graph (onSuccess/onFailure edges), or undefined
235
+ * for a simple/bootstrap-style job with no phases. Lets a caller derive
236
+ * graph facts (e.g. "which phase is this job's submission phase", issue
237
+ * #1276) without hardcoding phase names or duplicating job loading.
238
+ */
239
+ async getJobPhaseMap(jobName) {
240
+ const job = await this.getOrLoadJob(jobName);
241
+ return job?.metadata.phases;
242
+ }
243
+ /**
244
+ * True when a job's phase, with its includes resolved, itself instructs
245
+ * the agent to emit `evidence.reviewHandoff` (issue #1318 follow-up).
246
+ *
247
+ * The #1276 reviewHandoff gate fires for any job's graph-derived
248
+ * submission phase, but not every job family promises reviewHandoff at
249
+ * that phase — jobs built on `rich-review-artifact-contract.md`,
250
+ * `author-docx.md`, or `reporting-standards.md` (analysis reports, DOCX
251
+ * deliverables, operational setup jobs) use a different, legitimate
252
+ * completion contract and never mention the field, even resolved. Gating
253
+ * those unconditionally would reject a call the job's own instructions
254
+ * never told the agent to satisfy. This check reads the phase's actual
255
+ * resolved instructions rather than a hardcoded skill/job allowlist, so
256
+ * a new reviewHandoff-emitting skill (or a new non-reviewHandoff one)
257
+ * needs no update here — same "derive, don't hardcode" reasoning as
258
+ * `derivePredecessorPhase`.
259
+ */
260
+ async phasePromisesReviewHandoff(jobName, phaseId) {
261
+ const job = await this.getOrLoadJob(jobName);
262
+ const raw = job?.phases.get(phaseId);
263
+ if (!raw)
264
+ return false;
265
+ const resolved = await this.resolveIncludes(raw, job.path);
266
+ return resolved.includes('evidence.reviewHandoff');
267
+ }
233
268
  async getJobOverview(jobName) {
234
269
  const job = await this.getOrLoadJob(jobName);
235
270
  if (!job)
@@ -5,7 +5,8 @@
5
5
  * Pure helpers for validating the three evidence fields that FRAIM jobs must
6
6
  * emit on key seekMentoring calls so the Hub can render its review surfaces:
7
7
  *
8
- * - evidence.reviewHandoff (submit phase, status === "awaiting_mentor")
8
+ * - evidence.reviewHandoff (job's submission phase, status === "complete";
9
+ * see isSubmitPhase for how that phase is found)
9
10
  * - evidence.nextJobRecommendations (retrospective phase completion)
10
11
  * - evidence.delegationLedger (create-delegation-graph phase)
11
12
  *
@@ -26,17 +27,36 @@ exports.validateNextJobRecommendations = validateNextJobRecommendations;
26
27
  exports.validateDelegationLedger = validateDelegationLedger;
27
28
  exports.validateHandoffContracts = validateHandoffContracts;
28
29
  exports.buildHandoffRejectionMessage = buildHandoffRejectionMessage;
30
+ const resolve_phase_edge_1 = require("./resolve-phase-edge");
29
31
  // ---------------------------------------------------------------------------
30
32
  // Phase detection
31
33
  // ---------------------------------------------------------------------------
32
34
  /**
33
35
  * Returns true when a seekMentoring call is a submit-phase handoff requiring
34
- * a reviewHandoff evidence field. The canonical signal is status === "awaiting_mentor"
35
- * — set only by submit-phase jobs requesting a review gate — rather than a
36
- * hardcoded phase name list, which is fragile for personalized jobs.
36
+ * a reviewHandoff evidence field.
37
+ *
38
+ * Two triggers, either is sufficient:
39
+ * - `status === "awaiting_mentor"`: the original #916 signal. No published
40
+ * `seekMentoring` status permits this value today (confirmed by
41
+ * `spike/1157-approval-gate/s5-dead-submit-gate.js`), so this branch is
42
+ * provably unreachable in production — kept so a future status value, or a
43
+ * job that still emits it, is not silently unenforced.
44
+ * - `status === "complete"` at the job's own submission phase, derived from
45
+ * its real phase graph (issue #1276, gating-only rebuild of #1157 Change
46
+ * 1): the unique phase whose `onSuccess` targets `address-feedback`. This
47
+ * is the trigger that actually fires. No hardcoded phase-name list — a
48
+ * personalized job with a differently-named submission phase is still
49
+ * caught, and a job whose graph doesn't uniquely name one (`phases`
50
+ * omitted, or the derivation is ambiguous) is not gated at all rather than
51
+ * guessed at.
37
52
  */
38
- function isSubmitPhase(currentPhase, status) {
39
- return status === 'awaiting_mentor';
53
+ function isSubmitPhase(currentPhase, status, phases) {
54
+ if (status === 'awaiting_mentor')
55
+ return true;
56
+ if (status === 'complete' && phases) {
57
+ return (0, resolve_phase_edge_1.derivePredecessorPhase)(phases, resolve_phase_edge_1.FEEDBACK_PHASE_ID) === currentPhase;
58
+ }
59
+ return false;
40
60
  }
41
61
  /**
42
62
  * Returns true when a seekMentoring call is a retrospective completion.
@@ -221,7 +241,14 @@ function validateHandoffContracts(args) {
221
241
  const phase = args.currentPhase ?? '';
222
242
  const status = args.status ?? '';
223
243
  const evidence = args.evidence ?? {};
224
- if (isSubmitPhase(phase, status)) {
244
+ const findings = args.findings ?? {};
245
+ if (isSubmitPhase(phase, status, args.phases) && !evidence.reviewHandoff && findings.reviewHandoff) {
246
+ return ['reviewHandoff was found in findings but must be nested under evidence. Move reviewHandoff out of findings and into the top-level evidence object.'];
247
+ }
248
+ if (isRetrospectivePhase(phase, status) && !evidence.nextJobRecommendations && findings.nextJobRecommendations) {
249
+ return ['nextJobRecommendations was found in findings but must be nested under evidence. Move nextJobRecommendations out of findings and into the top-level evidence object.'];
250
+ }
251
+ if (isSubmitPhase(phase, status, args.phases)) {
225
252
  const errors = validateReviewHandoff(evidence.reviewHandoff);
226
253
  if (errors)
227
254
  return errors;
@@ -246,48 +273,48 @@ function validateHandoffContracts(args) {
246
273
  // ---------------------------------------------------------------------------
247
274
  // Rejection message builder
248
275
  // ---------------------------------------------------------------------------
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
- }
276
+ const ADDRESS_FEEDBACK_APPROVAL_SCHEMA = `\`\`\`javascript
277
+ evidence: {
278
+ approved: true // Set only after the manager explicitly approves via the Hub review action
279
+ }
253
280
  \`\`\``;
254
- const REVIEW_HANDOFF_SCHEMA = `\`\`\`javascript
255
- evidence: {
256
- reviewHandoff: {
257
- reviewRequired: true,
258
- reviewTarget: { kind: "pull_request", url: "<PR URL>" } | { kind: "artifact_set", files: ["<path>"] } | null,
259
- artifacts: [{ label: "<label>", path: "<path>", kind: "<kind>" }],
260
- summary: "<optional summary>",
261
- reviewActions: [
262
- { kind: "approve", label: "Approve" },
263
- { kind: "request_changes", label: "Request Changes" }
264
- ]
265
- }
266
- }
281
+ const REVIEW_HANDOFF_SCHEMA = `\`\`\`javascript
282
+ evidence: {
283
+ reviewHandoff: {
284
+ reviewRequired: true,
285
+ reviewTarget: { kind: "pull_request", url: "<PR URL>" } | { kind: "artifact_set", files: ["<path>"] } | null,
286
+ artifacts: [{ label: "<label>", path: "<path>", kind: "<kind>" }],
287
+ summary: "<optional summary>",
288
+ reviewActions: [
289
+ { kind: "approve", label: "Approve" },
290
+ { kind: "request_changes", label: "Request Changes" }
291
+ ]
292
+ }
293
+ }
267
294
  \`\`\``;
268
- const NEXT_JOB_RECOMMENDATIONS_SCHEMA = `\`\`\`javascript
269
- evidence: {
270
- nextJobRecommendations: [
271
- {
272
- jobId: "<job-slug>",
273
- label: "<human-readable label>",
274
- reason: "<optional: why this job is recommended>",
275
- contextSummary: "<optional: one sentence context for the next agent>"
276
- }
277
- // 0–3 entries; an empty array [] is valid
278
- ]
279
- }
295
+ const NEXT_JOB_RECOMMENDATIONS_SCHEMA = `\`\`\`javascript
296
+ evidence: {
297
+ nextJobRecommendations: [
298
+ {
299
+ jobId: "<job-slug>",
300
+ label: "<human-readable label>",
301
+ reason: "<optional: why this job is recommended>",
302
+ contextSummary: "<optional: one sentence context for the next agent>"
303
+ }
304
+ // 0–3 entries; an empty array [] is valid
305
+ ]
306
+ }
280
307
  \`\`\``;
281
- const DELEGATION_LEDGER_SCHEMA = `\`\`\`javascript
282
- evidence: {
283
- delegationLedger: {
284
- delegationRequired: true,
285
- objective: "<optional objective string>",
286
- tasks: [
287
- { jobId: "<job-slug>", personaKey: "<persona key or null>", briefing: "<optional briefing>" }
288
- ]
289
- }
290
- }
308
+ const DELEGATION_LEDGER_SCHEMA = `\`\`\`javascript
309
+ evidence: {
310
+ delegationLedger: {
311
+ delegationRequired: true,
312
+ objective: "<optional objective string>",
313
+ tasks: [
314
+ { jobId: "<job-slug>", personaKey: "<persona key or null>", briefing: "<optional briefing>" }
315
+ ]
316
+ }
317
+ }
291
318
  \`\`\``;
292
319
  const FIELD_SCHEMAS = {
293
320
  approved: ADDRESS_FEEDBACK_APPROVAL_SCHEMA,
@@ -15,9 +15,11 @@
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 = void 0;
18
19
  exports.resolvePhaseEdge = resolvePhaseEdge;
19
20
  exports.resolveDiscriminant = resolveDiscriminant;
20
21
  exports.discriminantKeys = discriminantKeys;
22
+ exports.derivePredecessorPhase = derivePredecessorPhase;
21
23
  /** The default discriminant, and the mandatory key on every authored map. */
22
24
  const DEFAULT_DISCRIMINANT = 'default';
23
25
  /**
@@ -73,3 +75,34 @@ function discriminantKeys(edge) {
73
75
  return [];
74
76
  return Object.keys(edge).filter((key) => key !== DEFAULT_DISCRIMINANT);
75
77
  }
78
+ /** The framework's one review/decision phase; every reviewable job routes into it. */
79
+ exports.FEEDBACK_PHASE_ID = 'address-feedback';
80
+ /**
81
+ * Finds the unique phase in a job's phase map whose `onSuccess` edge can
82
+ * resolve to `targetPhaseId` for some discriminant (including `default`).
83
+ *
84
+ * Gating-only rebuild of #1157 Change 1 ("graph-derived phase identity"),
85
+ * scoped per #1276: that change also added routing behavior and was reverted
86
+ * before merge because the two were bundled. Only the lookup survives here —
87
+ * it answers "which phase is this job's submission phase" without changing
88
+ * how any phase transition resolves. No hardcoded phase-name list: the
89
+ * derivation reads whatever the job's own graph declares, so it is correct
90
+ * for every current and future job without per-job edits.
91
+ *
92
+ * Returns `null` when zero or multiple phases match, so a caller can fail
93
+ * safe (treat as "unknown") rather than guess at an ambiguous graph.
94
+ */
95
+ function derivePredecessorPhase(phases, targetPhaseId) {
96
+ const predecessors = [];
97
+ for (const [phaseId, edges] of Object.entries(phases ?? {})) {
98
+ if (phaseId === targetPhaseId || !edges)
99
+ continue;
100
+ const edge = edges.onSuccess;
101
+ const targets = typeof edge === 'string'
102
+ ? [edge]
103
+ : (edge && typeof edge === 'object' ? Object.values(edge) : []);
104
+ if (targets.includes(targetPhaseId))
105
+ predecessors.push(phaseId);
106
+ }
107
+ return predecessors.length === 1 ? predecessors[0] : null;
108
+ }