sinapse-ai 1.6.1 → 1.8.0

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 (131) hide show
  1. package/.claude/CLAUDE.md +5 -11
  2. package/.claude/hooks/README.md +14 -1
  3. package/.claude/hooks/code-intel-pretool.cjs +115 -0
  4. package/.claude/hooks/enforce-delegation.cjs +31 -3
  5. package/.claude/hooks/enforce-framework-boundary.cjs +324 -0
  6. package/.claude/hooks/enforce-permission-mode.cjs +249 -0
  7. package/.claude/hooks/secret-scanning.cjs +34 -43
  8. package/.claude/hooks/synapse-engine.cjs +23 -23
  9. package/.claude/hooks/telemetry-post-tool.cjs +128 -0
  10. package/.claude/hooks/telemetry-stop.cjs +132 -0
  11. package/.claude/hooks/verify-packages.cjs +9 -2
  12. package/.claude/rules/documentation-first.md +1 -1
  13. package/.claude/rules/hook-governance.md +2 -0
  14. package/.sinapse-ai/cli/commands/health/index.js +24 -0
  15. package/.sinapse-ai/core/README.md +11 -0
  16. package/.sinapse-ai/core/config/config-loader.js +19 -0
  17. package/.sinapse-ai/core/config/merge-utils.js +8 -0
  18. package/.sinapse-ai/core/errors/constants.js +147 -0
  19. package/.sinapse-ai/core/errors/error-registry.js +176 -0
  20. package/.sinapse-ai/core/errors/index.js +50 -0
  21. package/.sinapse-ai/core/errors/serializer.js +147 -0
  22. package/.sinapse-ai/core/errors/sinapse-error.js +144 -0
  23. package/.sinapse-ai/core/errors/utils.js +187 -0
  24. package/.sinapse-ai/core/execution/build-orchestrator.js +47 -49
  25. package/.sinapse-ai/core/execution/build-state-manager.js +183 -31
  26. package/.sinapse-ai/core/execution/parallel-executor.js +7 -1
  27. package/.sinapse-ai/core/execution/semantic-merge-engine.js +26 -14
  28. package/.sinapse-ai/core/execution/subagent-dispatcher.js +201 -60
  29. package/.sinapse-ai/core/execution/wave-executor.js +4 -1
  30. package/.sinapse-ai/core/grounding/README.md +71 -11
  31. package/.sinapse-ai/core/health-check/checks/project/framework-config.js +38 -2
  32. package/.sinapse-ai/core/health-check/checks/project/package-json.js +47 -3
  33. package/.sinapse-ai/core/health-check/checks/services/gemini-cli.js +117 -0
  34. package/.sinapse-ai/core/health-check/checks/services/index.js +2 -0
  35. package/.sinapse-ai/core/health-check/healers/index.js +40 -3
  36. package/.sinapse-ai/core/ideation/ideation-engine.js +212 -107
  37. package/.sinapse-ai/core/ids/gate-evaluator.js +318 -0
  38. package/.sinapse-ai/core/ids/gates/g5-semantic-handshake.js +190 -0
  39. package/.sinapse-ai/core/ids/gates/g6-ci-integrity.js +162 -0
  40. package/.sinapse-ai/core/ids/index.js +30 -0
  41. package/.sinapse-ai/core/memory/__tests__/active-modules.verify.js +11 -0
  42. package/.sinapse-ai/core/memory/gotchas-memory.js +37 -2
  43. package/.sinapse-ai/core/orchestration/agent-invoker.js +29 -6
  44. package/.sinapse-ai/core/orchestration/brownfield-handler.js +36 -3
  45. package/.sinapse-ai/core/orchestration/condition-evaluator.js +57 -0
  46. package/.sinapse-ai/core/orchestration/executors/epic-3-executor.js +76 -5
  47. package/.sinapse-ai/core/orchestration/executors/epic-4-executor.js +63 -17
  48. package/.sinapse-ai/core/orchestration/executors/epic-6-executor.js +153 -41
  49. package/.sinapse-ai/core/orchestration/executors/epic-executor.js +40 -0
  50. package/.sinapse-ai/core/orchestration/greenfield-handler.js +87 -3
  51. package/.sinapse-ai/core/orchestration/master-orchestrator.js +150 -10
  52. package/.sinapse-ai/core/orchestration/parallel-executor.js +6 -1
  53. package/.sinapse-ai/core/orchestration/recovery-handler.js +81 -8
  54. package/.sinapse-ai/core/orchestration/workflow-executor.js +41 -0
  55. package/.sinapse-ai/core/registry/registry-loader.js +71 -5
  56. package/.sinapse-ai/core/registry/squad-agent-resolver.js +253 -0
  57. package/.sinapse-ai/core/synapse/context/context-tracker.js +104 -9
  58. package/.sinapse-ai/core/synapse/context/index.js +19 -0
  59. package/.sinapse-ai/core/synapse/context/semantic-handshake-engine.js +555 -0
  60. package/.sinapse-ai/core/synapse/diagnostics/collectors/pipeline-collector.js +4 -2
  61. package/.sinapse-ai/core/synapse/engine.js +43 -3
  62. package/.sinapse-ai/core/telemetry/ids-sink.js +188 -0
  63. package/.sinapse-ai/core/utils/output-formatter.js +8 -290
  64. package/.sinapse-ai/core/utils/spawn-safe.js +186 -0
  65. package/.sinapse-ai/core-config.yaml +68 -1
  66. package/.sinapse-ai/data/entity-registry.yaml +15082 -13618
  67. package/.sinapse-ai/data/registry-update-log.jsonl +143 -0
  68. package/.sinapse-ai/development/agents/developer.md +2 -0
  69. package/.sinapse-ai/development/agents/devops.md +9 -0
  70. package/.sinapse-ai/development/external-executors/README.md +18 -0
  71. package/.sinapse-ai/development/external-executors/codex.md +56 -0
  72. package/.sinapse-ai/development/scripts/populate-entity-registry.js +65 -9
  73. package/.sinapse-ai/development/scripts/squad/squad-downloader.js +169 -14
  74. package/.sinapse-ai/development/tasks/delegate-to-external-executor.md +152 -0
  75. package/.sinapse-ai/development/tasks/github-devops-pre-push-quality-gate.md +46 -29
  76. package/.sinapse-ai/development/tasks/update-sinapse.md +3 -3
  77. package/.sinapse-ai/hooks/sinapse-brand-grounding.cjs +4 -7
  78. package/.sinapse-ai/hooks/sinapse-ds-grounding.cjs +5 -8
  79. package/.sinapse-ai/hooks/sinapse-vault-grounding.cjs +6 -9
  80. package/.sinapse-ai/infrastructure/integrations/ai-providers/ai-provider-factory.js +4 -1
  81. package/.sinapse-ai/infrastructure/integrations/ai-providers/claude-provider.js +57 -55
  82. package/.sinapse-ai/infrastructure/integrations/pm-adapters/github-adapter.js +9 -7
  83. package/.sinapse-ai/infrastructure/scripts/ide-sync/gemini-commands.js +298 -0
  84. package/.sinapse-ai/infrastructure/scripts/ide-sync/index.js +127 -6
  85. package/.sinapse-ai/infrastructure/scripts/ide-sync/persona-renderer.js +97 -0
  86. package/.sinapse-ai/infrastructure/scripts/ide-sync/transformers/antigravity.js +121 -0
  87. package/.sinapse-ai/infrastructure/scripts/ide-sync/transformers/cursor.js +119 -0
  88. package/.sinapse-ai/infrastructure/scripts/ide-sync/transformers/github-copilot.js +191 -0
  89. package/.sinapse-ai/infrastructure/scripts/ide-sync/transformers/kimi.js +448 -0
  90. package/.sinapse-ai/install-manifest.yaml +218 -114
  91. package/.sinapse-ai/product/templates/engine/renderer.js +20 -1
  92. package/.sinapse-ai/scripts/pm.sh +18 -6
  93. package/bin/cli.js +17 -0
  94. package/bin/commands/agents.js +96 -0
  95. package/bin/commands/doctor.js +15 -0
  96. package/bin/commands/ideate.js +129 -0
  97. package/bin/commands/uninstall.js +40 -0
  98. package/bin/postinstall.js +50 -4
  99. package/bin/sinapse.js +146 -2
  100. package/bin/utils/secret-scanner-core.js +253 -0
  101. package/bin/utils/staged-secret-scan.js +106 -40
  102. package/docs/framework/collaboration-autonomy-plan.md +18 -18
  103. package/docs/guides/parallel-workflow.md +6 -6
  104. package/package.json +22 -5
  105. package/packages/installer/src/installer/git-hooks-installer.js +384 -0
  106. package/packages/installer/src/installer/sinapse-ai-installer.js +16 -0
  107. package/packages/installer/src/wizard/ide-config-generator.js +23 -0
  108. package/packages/installer/src/wizard/validators.js +38 -1
  109. package/packages/installer/tests/unit/artifact-copy-pipeline/artifact-copy-pipeline.test.js +5 -1
  110. package/packages/installer/tests/unit/doctor/doctor-checks.test.js +44 -22
  111. package/packages/installer/tests/unit/git-hooks-installer.test.js +262 -0
  112. package/scripts/eval-runner.js +422 -0
  113. package/scripts/generate-install-manifest.js +13 -9
  114. package/scripts/generate-synapse-runtime.js +51 -0
  115. package/scripts/regenerate-orqx-stubs.ps1 +6 -5
  116. package/scripts/validate-all.js +1 -0
  117. package/scripts/validate-evals.js +466 -0
  118. package/scripts/validate-schemas.js +539 -0
  119. package/scripts/validate-squad-orqx.js +9 -2
  120. package/squads/claude-code-mastery/knowledge-base/memory-systems-reference.md +1 -1
  121. package/squads/squad-brand/templates/client-delivery-template.md +1 -1
  122. package/squads/squad-content/knowledge-base/social-compression-framework.md +1 -1
  123. package/squads/squad-council/knowledge-base/brand-strategy-models.md +1 -1
  124. package/.sinapse-ai/development/scripts/elicitation-engine.js +0 -385
  125. package/.sinapse-ai/development/scripts/elicitation-session-manager.js +0 -300
  126. package/.sinapse-ai/development/tasks/test-validation-task.md +0 -172
  127. package/docs/chrome-brain-upgrade-plan.md +0 -624
  128. package/docs/constitution-compliance.md +0 -87
  129. package/docs/mega-upgrade-orchestration-plan.md +0 -71
  130. package/docs/research-synthesis-for-upgrade.md +0 -511
  131. package/docs/security-audit-report.md +0 -306
@@ -37,20 +37,12 @@ class Epic6Executor extends EpicExecutor {
37
37
  this._qaOrchestrator = null;
38
38
  }
39
39
 
40
- /**
41
- * Get QA Loop Orchestrator
42
- * @private
43
- */
44
- _getQAOrchestrator() {
45
- if (!this._qaOrchestrator) {
46
- try {
47
- this._qaOrchestrator = require('../../infrastructure/scripts/qa-loop-orchestrator');
48
- } catch (error) {
49
- this._log(`QA Loop Orchestrator not available: ${error.message}`, 'warn');
50
- }
51
- }
52
- return this._qaOrchestrator;
53
- }
40
+ // NOTE: the previous `_getQAOrchestrator()` path was triple-broken theater —
41
+ // it did `new` on the module object (not the QALoopOrchestrator class), passed
42
+ // the wrong constructor shape, and called a `runReview()` method that doesn't
43
+ // exist. It ALWAYS threw and silently fell back to basic checks while reporting
44
+ // success:true. Replaced by a real @quality-gate agent invocation (_reviewViaAgent),
45
+ // same proven path as epic-3 spec generation (epic: orchestration-consolidation).
54
46
 
55
47
  /**
56
48
  * Execute QA Loop
@@ -98,7 +90,9 @@ class Epic6Executor extends EpicExecutor {
98
90
 
99
91
  if (currentVerdict === QAVerdict.NEEDS_REVISION && iteration < this.maxIterations) {
100
92
  this._log('Review needs revision, applying fixes...');
101
- await this._applyFixes(reviewResult.issues, context);
93
+ // Record whether fixes were really applied (honesty invariant): a
94
+ // stubbed fix must not look like a real one in the QA report.
95
+ reviewResult.fixResult = await this._applyFixes(reviewResult.issues, context);
102
96
  }
103
97
  }
104
98
 
@@ -107,53 +101,60 @@ class Epic6Executor extends EpicExecutor {
107
101
  this._addArtifact('qa-report', reportPath);
108
102
 
109
103
  const passed = currentVerdict === QAVerdict.APPROVED;
104
+ const anyRealReview = reviewHistory.some((r) => r.reviewSource === 'agent');
110
105
 
111
- return this._completeExecution({
106
+ const baseResult = {
112
107
  verdict: currentVerdict,
113
108
  passed,
114
109
  iterations: iteration,
115
110
  reviewHistory,
116
111
  reportPath,
117
- });
112
+ };
113
+
114
+ // Honesty invariant (F0a/F7): a QA loop that only ran deterministic basic
115
+ // checks did NOT really review the work — it must report STUB, not success.
116
+ if (!anyRealReview) {
117
+ return this._stubExecution(
118
+ 'QA ran deterministic basic checks only — no real review agent wired',
119
+ baseResult,
120
+ );
121
+ }
122
+
123
+ return this._completeExecution(baseResult);
118
124
  } catch (error) {
119
125
  return this._failExecution(error);
120
126
  }
121
127
  }
122
128
 
123
129
  /**
124
- * Run a single review cycle
130
+ * Run a single review cycle.
131
+ *
132
+ * Tries a REAL review via the @quality-gate agent (orchestrator.invokeAgent →
133
+ * SubagentDispatcher → claude). Falls back to deterministic basic checks when
134
+ * no executor is wired or real execution isn't allowed — and marks the source
135
+ * (`agent` vs `basic-checks`) so the epic can report honestly.
125
136
  * @private
126
137
  */
127
138
  async _runReview(context) {
128
- const { storyId, iteration, techStack: _techStack } = context;
129
-
139
+ const { iteration } = context;
130
140
  this._log(`Running review cycle ${iteration}`);
131
141
 
132
- // Check for QA orchestrator
133
- const QAOrchestrator = this._getQAOrchestrator();
134
-
135
- if (QAOrchestrator) {
142
+ const invokeAgent = this.orchestrator && this.orchestrator.invokeAgent;
143
+ if (typeof invokeAgent === 'function' && this._realExecutionAllowed()) {
136
144
  try {
137
- const orchestrator = new QAOrchestrator({
138
- storyId,
139
- rootPath: this.projectRoot,
140
- });
141
-
142
- return await orchestrator.runReview(context);
145
+ const realReview = await this._reviewViaAgent(invokeAgent, context);
146
+ if (realReview) return realReview;
143
147
  } catch (error) {
144
- this._log(`QA orchestrator error: ${error.message}`, 'warn');
148
+ this._log(`Real review failed, falling back to basic checks: ${error.message}`, 'warn');
145
149
  }
146
150
  }
147
151
 
148
- // Fallback: perform basic checks
152
+ // Honest fallback: deterministic basic checks (clearly marked as a stub source).
149
153
  const issues = await this._performBasicChecks(context);
150
154
 
151
- // Determine verdict based on issues
152
155
  let verdict = QAVerdict.APPROVED;
153
-
154
156
  const criticalIssues = issues.filter((i) => i.severity === 'critical');
155
157
  const majorIssues = issues.filter((i) => i.severity === 'major');
156
-
157
158
  if (criticalIssues.length > 0) {
158
159
  verdict = QAVerdict.BLOCKED;
159
160
  } else if (majorIssues.length > 0 || issues.length > 5) {
@@ -164,10 +165,71 @@ class Epic6Executor extends EpicExecutor {
164
165
  iteration,
165
166
  verdict,
166
167
  issues,
168
+ reviewSource: 'basic-checks',
167
169
  timestamp: new Date().toISOString(),
168
170
  };
169
171
  }
170
172
 
173
+ /**
174
+ * Perform a real QA review by invoking the @quality-gate agent.
175
+ * @param {Function} invokeAgent - orchestrator.invokeAgent
176
+ * @param {Object} context - review context
177
+ * @returns {Promise<Object|null>} review result with verdict, or null when the
178
+ * agent produced nothing usable (caller falls back to basic checks).
179
+ * @private
180
+ */
181
+ async _reviewViaAgent(invokeAgent, context) {
182
+ const { storyId, iteration, codeChanges, testResults } = context;
183
+
184
+ const description =
185
+ `Perform a QA review for story "${storyId || 'unknown'}". ` +
186
+ 'Assess correctness, test coverage, and adherence to acceptance criteria. ' +
187
+ `${Array.isArray(codeChanges) ? `${codeChanges.length} code change(s). ` : ''}` +
188
+ `${Array.isArray(testResults) ? `${testResults.length} test result(s). ` : ''}` +
189
+ 'End your response with a single verdict line in the form: ' +
190
+ 'VERDICT: APPROVED | NEEDS_REVISION | BLOCKED.';
191
+
192
+ const result = await invokeAgent(
193
+ { name: 'quality-gate' },
194
+ { id: `qa-review-${storyId || 'story'}-${iteration}`, type: 'review', description },
195
+ { storyId, iteration },
196
+ );
197
+
198
+ const output = result && (result.output || result.content);
199
+ if (!result || result.success === false || !output || output.trim().length === 0) {
200
+ return null;
201
+ }
202
+
203
+ return {
204
+ iteration,
205
+ verdict: this._parseVerdict(output),
206
+ issues: [],
207
+ reviewSource: 'agent',
208
+ raw: output,
209
+ timestamp: new Date().toISOString(),
210
+ };
211
+ }
212
+
213
+ /**
214
+ * Parse a QA verdict from free-form agent output. Defaults to NEEDS_REVISION
215
+ * (the safe, non-approving default) when no explicit verdict is found.
216
+ * @param {string} output
217
+ * @returns {string} QAVerdict
218
+ * @private
219
+ */
220
+ _parseVerdict(output) {
221
+ const text = String(output).toUpperCase();
222
+ const m = text.match(/VERDICT:\s*(APPROVED|NEEDS[_\s]?REVISION|BLOCKED)/);
223
+ const token = (m ? m[1] : '').replace(/\s/g, '_');
224
+ if (token === 'APPROVED') return QAVerdict.APPROVED;
225
+ if (token === 'BLOCKED') return QAVerdict.BLOCKED;
226
+ if (token === 'NEEDS_REVISION') return QAVerdict.NEEDS_REVISION;
227
+ // No explicit verdict → don't fabricate approval.
228
+ if (/\bBLOCKED\b/.test(text)) return QAVerdict.BLOCKED;
229
+ if (/\bAPPROVED\b/.test(text)) return QAVerdict.APPROVED;
230
+ return QAVerdict.NEEDS_REVISION;
231
+ }
232
+
171
233
  /**
172
234
  * Perform basic quality checks
173
235
  * @private
@@ -193,17 +255,67 @@ class Epic6Executor extends EpicExecutor {
193
255
  }
194
256
 
195
257
  /**
196
- * Apply fixes for found issues
258
+ * Apply fixes for found issues by invoking the real @developer agent through
259
+ * the orchestrator (orchestrator.invokeAgent → SubagentDispatcher → claude).
260
+ *
261
+ * epic: orchestration-consolidation — connects Epic 6 to the real executor,
262
+ * same pattern as Epic 3. Falls back honestly (logs that no real fix ran) when
263
+ * no executor is wired or real execution isn't allowed in this environment.
264
+ *
265
+ * @param {Array} issues - Issues found by the review
266
+ * @param {Object} context - Execution context (storyId, techStack, ...)
267
+ * @returns {Promise<Object>} { applied: boolean, fixed: number, stub: boolean }
197
268
  * @private
198
269
  */
199
- async _applyFixes(issues, _context) {
270
+ async _applyFixes(issues, context = {}) {
200
271
  this._log(`Applying fixes for ${issues.length} issues`);
201
272
 
202
- // In full implementation, this would invoke @developer agent
203
- // to fix each issue. For now, just log.
204
- for (const issue of issues) {
205
- this._log(`Would fix: ${issue.type} - ${issue.message}`);
273
+ const invokeAgent = this.orchestrator && this.orchestrator.invokeAgent;
274
+ const canRunReal = typeof invokeAgent === 'function' && this._realExecutionAllowed();
275
+
276
+ if (!canRunReal || issues.length === 0) {
277
+ // Honest fallback: do NOT claim fixes were applied.
278
+ for (const issue of issues) {
279
+ this._log(`Fix not applied (no real executor): ${issue.type} - ${issue.message}`, 'warn');
280
+ }
281
+ return { applied: false, fixed: 0, stub: true };
282
+ }
283
+
284
+ const issueList = issues
285
+ .map((i, n) => `${n + 1}. [${i.severity || 'unknown'}] ${i.type}: ${i.message}`)
286
+ .join('\n');
287
+
288
+ const description =
289
+ `Fix the following QA issues found during review of story "${context.storyId || 'unknown'}". ` +
290
+ 'Apply the minimal, correct change for each; preserve existing patterns and tests.\n\n' +
291
+ `Issues:\n${issueList}`;
292
+
293
+ try {
294
+ const result = await invokeAgent(
295
+ { name: 'developer' },
296
+ { id: `qa-fix-${context.storyId || 'story'}`, type: 'bugfix', description },
297
+ {
298
+ storyId: context.storyId,
299
+ techStack: context.techStack,
300
+ issues: issues.map((i) => ({ type: i.type, severity: i.severity, message: i.message })),
301
+ },
302
+ );
303
+
304
+ if (result && result.success !== false) {
305
+ this._log(`Applied fixes via real agent (${issues.length} issues addressed)`);
306
+ return {
307
+ applied: true,
308
+ fixed: issues.length,
309
+ stub: false,
310
+ filesModified: result.filesModified || [],
311
+ };
312
+ }
313
+ this._log(`Fix agent returned no success: ${result && result.error}`, 'warn');
314
+ } catch (err) {
315
+ this._log(`Fix agent invocation failed: ${err.message}`, 'warn');
206
316
  }
317
+
318
+ return { applied: false, fixed: 0, stub: true };
207
319
  }
208
320
 
209
321
  /**
@@ -22,6 +22,7 @@ const ExecutionStatus = {
22
22
  SUCCESS: 'success',
23
23
  FAILED: 'failed',
24
24
  SKIPPED: 'skipped',
25
+ STUB: 'stub',
25
26
  };
26
27
 
27
28
  /**
@@ -146,6 +147,45 @@ class EpicExecutor {
146
147
  };
147
148
  }
148
149
 
150
+ /**
151
+ * Complete execution in STUB mode — work was NOT actually performed.
152
+ *
153
+ * Honesty invariant (epic: orchestration-consolidation, Frente F0a):
154
+ * a stub MUST NOT report success:true. getResult() yields success:false
155
+ * because this.status !== SUCCESS.
156
+ *
157
+ * @param {string} reason - What real work is missing
158
+ * @param {Object} [result] - Optional result data to merge
159
+ * @protected
160
+ */
161
+ _stubExecution(reason, result = {}) {
162
+ this.status = ExecutionStatus.STUB;
163
+ this.endTime = new Date().toISOString();
164
+ this._log(`Epic ${this.epicNum} ran in STUB mode (no real work performed): ${reason}`, 'warn');
165
+
166
+ return {
167
+ ...this.getResult(),
168
+ status: ExecutionStatus.STUB,
169
+ stub: true,
170
+ stubReason: reason,
171
+ ...result,
172
+ };
173
+ }
174
+
175
+ /**
176
+ * Whether real execution (spawning claude / running the real build) is allowed.
177
+ * Returns false inside the test runner unless SINAPSE_REAL_DISPATCH=1 — this
178
+ * prevents slow, costly, non-deterministic CLI/build calls during unit tests.
179
+ * epic: orchestration-consolidation, F1.
180
+ * @returns {boolean}
181
+ * @protected
182
+ */
183
+ _realExecutionAllowed() {
184
+ return (
185
+ process.env.JEST_WORKER_ID === undefined || process.env.SINAPSE_REAL_DISPATCH === '1'
186
+ );
187
+ }
188
+
149
189
  /**
150
190
  * Add artifact to results
151
191
  * @param {string} type - Artifact type (file, report, data)
@@ -39,6 +39,35 @@ const DEFAULT_GREENFIELD_INDICATORS = [
39
39
  '.sinapse-ai/',
40
40
  ];
41
41
 
42
+ /**
43
+ * Maps a classified project_type to its greenfield workflow variant. The
44
+ * variants stay separate files (distinct phase/agent contracts — fusion was
45
+ * refuted in the deep-dive); the handler just dispatches to the right one.
46
+ * Unknown/absent type falls back to fullstack (the historical default).
47
+ * @see project-intelligence.md classification matrix
48
+ */
49
+ const GREENFIELD_WORKFLOW_BY_TYPE = Object.freeze({
50
+ site: 'greenfield-ui',
51
+ lp: 'greenfield-ui',
52
+ app: 'greenfield-ui',
53
+ platform: 'greenfield-fullstack',
54
+ saas: 'greenfield-fullstack',
55
+ api: 'greenfield-service',
56
+ service: 'greenfield-service',
57
+ });
58
+
59
+ const DEFAULT_GREENFIELD_WORKFLOW = 'greenfield-fullstack';
60
+
61
+ /**
62
+ * Resolve the greenfield workflow basename for a project_type.
63
+ * @param {string} [projectType]
64
+ * @returns {string} workflow name (without extension)
65
+ */
66
+ function resolveGreenfieldWorkflow(projectType) {
67
+ const key = String(projectType || '').toLowerCase();
68
+ return GREENFIELD_WORKFLOW_BY_TYPE[key] || DEFAULT_GREENFIELD_WORKFLOW;
69
+ }
70
+
42
71
  /**
43
72
  * Greenfield workflow phases
44
73
  * @enum {string}
@@ -112,10 +141,12 @@ class GreenfieldHandler extends EventEmitter {
112
141
  this._surfaceChecker = options.surfaceChecker || null;
113
142
  this._sessionState = options.sessionState || null;
114
143
 
115
- // Workflow path
144
+ // Workflow variant dispatched by project_type (site/lp/app → ui,
145
+ // platform/saas → fullstack, api/service → service). Default fullstack.
146
+ this.workflowName = resolveGreenfieldWorkflow(options.projectType);
116
147
  this.workflowPath = path.join(
117
148
  projectRoot,
118
- '.sinapse-ai/development/workflows/greenfield-fullstack.yaml',
149
+ `.sinapse-ai/development/workflows/${this.workflowName}.yaml`,
119
150
  );
120
151
 
121
152
  // Phase progress tracking
@@ -224,6 +255,12 @@ class GreenfieldHandler extends EventEmitter {
224
255
  async handle(context = {}) {
225
256
  this._log('Greenfield handler invoked');
226
257
 
258
+ // Stream B (Frente 4.2): ensure the secret-scan git guard is wired into the
259
+ // target project. Runs on EVERY entry (Phase 0 bootstrap AND the
260
+ // skip-bootstrap / resume paths) so a project that already has package.json
261
+ // + .git still gets the guard. Best-effort, idempotent, never throws.
262
+ await this._ensureGitHooks();
263
+
227
264
  // Check for resume (AC14)
228
265
  const resumePhase = context.resumeFromPhase;
229
266
  if (typeof resumePhase === 'number' && resumePhase >= 0) {
@@ -273,6 +310,51 @@ class GreenfieldHandler extends EventEmitter {
273
310
  }
274
311
  }
275
312
 
313
+ /**
314
+ * Ensures the SINAPSE secret-scan git guard is installed in the project.
315
+ *
316
+ * Stream B (Frente 4.2): wires `core.hooksPath` to a managed Node pre-commit
317
+ * hook so every greenfield project the framework bootstraps blocks staged
318
+ * secrets. Best-effort and idempotent — re-running never duplicates hooks and
319
+ * a failure here never aborts the workflow.
320
+ *
321
+ * @returns {Promise<boolean>} true if the guard is wired
322
+ * @private
323
+ */
324
+ async _ensureGitHooks() {
325
+ // Resolve the installer across both layouts: the sinapse-ai dev repo
326
+ // (`<root>/packages/installer/...`) and an installed project copy (where
327
+ // the framework lives under `.sinapse-ai/`). First match wins.
328
+ const candidates = [
329
+ path.join(this.projectRoot, 'packages', 'installer', 'src', 'installer', 'git-hooks-installer'),
330
+ path.join(this.projectRoot, '.sinapse-ai', 'packages', 'installer', 'src', 'installer', 'git-hooks-installer'),
331
+ ];
332
+
333
+ let installGitHooks = null;
334
+ for (const candidate of candidates) {
335
+ try {
336
+ ({ installGitHooks } = require(candidate));
337
+ if (typeof installGitHooks === 'function') break;
338
+ } catch {
339
+ // Try the next candidate.
340
+ }
341
+ }
342
+
343
+ if (typeof installGitHooks !== 'function') {
344
+ this._log('Git secret-scan guard not installed: git-hooks-installer not found', 'warn');
345
+ return false;
346
+ }
347
+
348
+ try {
349
+ const res = await installGitHooks({ projectDir: this.projectRoot });
350
+ this._log(`Git secret-scan guard: ${res.success ? 'installed' : 'skipped'}${res.error ? ` (${res.error})` : ''}`);
351
+ return res.success;
352
+ } catch (error) {
353
+ this._log(`Git secret-scan guard not installed: ${error.message}`, 'warn');
354
+ return false;
355
+ }
356
+ }
357
+
276
358
  // ═══════════════════════════════════════════════════════════════════════════════════
277
359
  // PHASE 0: BOOTSTRAP (AC2)
278
360
  // ═══════════════════════════════════════════════════════════════════════════════════
@@ -820,7 +902,7 @@ class GreenfieldHandler extends EventEmitter {
820
902
  const exists = await sessionState.exists();
821
903
  if (exists) {
822
904
  await sessionState.loadSessionState();
823
- await sessionState.recordPhaseChange(`greenfield_${phase}`, 'greenfield-fullstack', '@pm');
905
+ await sessionState.recordPhaseChange(`greenfield_${phase}`, this.workflowName, '@pm');
824
906
  this._log(`Phase recorded in session state: ${phase}`);
825
907
  }
826
908
  } catch (error) {
@@ -944,5 +1026,7 @@ module.exports = {
944
1026
  PhaseFailureAction,
945
1027
  DEFAULT_GREENFIELD_INDICATORS,
946
1028
  PHASE_1_SEQUENCE,
1029
+ GREENFIELD_WORKFLOW_BY_TYPE,
1030
+ resolveGreenfieldWorkflow,
947
1031
  };
948
1032