@prism-d1/cli 1.0.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 (85) hide show
  1. package/dist/bin/prism-cli.d.ts +3 -0
  2. package/dist/bin/prism-cli.d.ts.map +1 -0
  3. package/dist/bin/prism-cli.js +10 -0
  4. package/dist/bin/prism-cli.js.map +1 -0
  5. package/dist/src/commands/assessment/deploy-cdk.d.ts +22 -0
  6. package/dist/src/commands/assessment/deploy-cdk.d.ts.map +1 -0
  7. package/dist/src/commands/assessment/deploy-cdk.js +59 -0
  8. package/dist/src/commands/assessment/deploy-cdk.js.map +1 -0
  9. package/dist/src/commands/assessment/interview-agent.d.ts +71 -0
  10. package/dist/src/commands/assessment/interview-agent.d.ts.map +1 -0
  11. package/dist/src/commands/assessment/interview-agent.js +648 -0
  12. package/dist/src/commands/assessment/interview-agent.js.map +1 -0
  13. package/dist/src/commands/assessment/run.d.ts +20 -0
  14. package/dist/src/commands/assessment/run.d.ts.map +1 -0
  15. package/dist/src/commands/assessment/run.js +23 -0
  16. package/dist/src/commands/assessment/run.js.map +1 -0
  17. package/dist/src/commands/assessment/web.d.ts +15 -0
  18. package/dist/src/commands/assessment/web.d.ts.map +1 -0
  19. package/dist/src/commands/assessment/web.js +1393 -0
  20. package/dist/src/commands/assessment/web.js.map +1 -0
  21. package/dist/src/commands/bootstrapper/install-eval-harness.d.ts +15 -0
  22. package/dist/src/commands/bootstrapper/install-eval-harness.d.ts.map +1 -0
  23. package/dist/src/commands/bootstrapper/install-eval-harness.js +99 -0
  24. package/dist/src/commands/bootstrapper/install-eval-harness.js.map +1 -0
  25. package/dist/src/commands/bootstrapper/install-git-hooks.d.ts +15 -0
  26. package/dist/src/commands/bootstrapper/install-git-hooks.d.ts.map +1 -0
  27. package/dist/src/commands/bootstrapper/install-git-hooks.js +141 -0
  28. package/dist/src/commands/bootstrapper/install-git-hooks.js.map +1 -0
  29. package/dist/src/commands/bootstrapper/setup-github-oidc.d.ts +6 -0
  30. package/dist/src/commands/bootstrapper/setup-github-oidc.d.ts.map +1 -0
  31. package/dist/src/commands/bootstrapper/setup-github-oidc.js +157 -0
  32. package/dist/src/commands/bootstrapper/setup-github-oidc.js.map +1 -0
  33. package/dist/src/commands/securityagent/setup.d.ts +14 -0
  34. package/dist/src/commands/securityagent/setup.d.ts.map +1 -0
  35. package/dist/src/commands/securityagent/setup.js +113 -0
  36. package/dist/src/commands/securityagent/setup.js.map +1 -0
  37. package/dist/src/commands/workshop/deploy-infra.d.ts +13 -0
  38. package/dist/src/commands/workshop/deploy-infra.d.ts.map +1 -0
  39. package/dist/src/commands/workshop/deploy-infra.js +73 -0
  40. package/dist/src/commands/workshop/deploy-infra.js.map +1 -0
  41. package/dist/src/commands/workshop/generate-demo-data.d.ts +16 -0
  42. package/dist/src/commands/workshop/generate-demo-data.d.ts.map +1 -0
  43. package/dist/src/commands/workshop/generate-demo-data.js +362 -0
  44. package/dist/src/commands/workshop/generate-demo-data.js.map +1 -0
  45. package/dist/src/commands/workshop/perform-pen-test.d.ts +14 -0
  46. package/dist/src/commands/workshop/perform-pen-test.d.ts.map +1 -0
  47. package/dist/src/commands/workshop/perform-pen-test.js +301 -0
  48. package/dist/src/commands/workshop/perform-pen-test.js.map +1 -0
  49. package/dist/src/commands/workshop/run-agent.d.ts +20 -0
  50. package/dist/src/commands/workshop/run-agent.d.ts.map +1 -0
  51. package/dist/src/commands/workshop/run-agent.js +89 -0
  52. package/dist/src/commands/workshop/run-agent.js.map +1 -0
  53. package/dist/src/commands/workshop/verify-setup.d.ts +14 -0
  54. package/dist/src/commands/workshop/verify-setup.d.ts.map +1 -0
  55. package/dist/src/commands/workshop/verify-setup.js +493 -0
  56. package/dist/src/commands/workshop/verify-setup.js.map +1 -0
  57. package/dist/src/index.d.ts +13 -0
  58. package/dist/src/index.d.ts.map +1 -0
  59. package/dist/src/index.js +44 -0
  60. package/dist/src/index.js.map +1 -0
  61. package/dist/src/scanner/categories.d.ts +8 -0
  62. package/dist/src/scanner/categories.d.ts.map +1 -0
  63. package/dist/src/scanner/categories.js +322 -0
  64. package/dist/src/scanner/categories.js.map +1 -0
  65. package/dist/src/scanner/index.d.ts +16 -0
  66. package/dist/src/scanner/index.d.ts.map +1 -0
  67. package/dist/src/scanner/index.js +59 -0
  68. package/dist/src/scanner/index.js.map +1 -0
  69. package/dist/src/scanner/reporter.d.ts +3 -0
  70. package/dist/src/scanner/reporter.d.ts.map +1 -0
  71. package/dist/src/scanner/reporter.js +102 -0
  72. package/dist/src/scanner/reporter.js.map +1 -0
  73. package/dist/src/scanner/scoring.d.ts +7 -0
  74. package/dist/src/scanner/scoring.d.ts.map +1 -0
  75. package/dist/src/scanner/scoring.js +77 -0
  76. package/dist/src/scanner/scoring.js.map +1 -0
  77. package/dist/src/scanner/types.d.ts +41 -0
  78. package/dist/src/scanner/types.d.ts.map +1 -0
  79. package/dist/src/scanner/types.js +2 -0
  80. package/dist/src/scanner/types.js.map +1 -0
  81. package/dist/src/scanner/utils.d.ts +16 -0
  82. package/dist/src/scanner/utils.d.ts.map +1 -0
  83. package/dist/src/scanner/utils.js +54 -0
  84. package/dist/src/scanner/utils.js.map +1 -0
  85. package/package.json +39 -0
@@ -0,0 +1,648 @@
1
+ /**
2
+ * PRISM D1 Velocity -- Interview Agent
3
+ *
4
+ * Conversational AI agent that conducts the SA interview via chat.
5
+ * Uses Amazon Bedrock (Claude) to ask questions, probe responses,
6
+ * and score answers against the rubrics from interview-guide.md.
7
+ *
8
+ * Runs locally — no AgentCore required. Just needs AWS credentials
9
+ * with Bedrock invoke-model access.
10
+ */
11
+ // ---------------------------------------------------------------------------
12
+ // Interview sections — same data as web.ts, extracted for agent use
13
+ // ---------------------------------------------------------------------------
14
+ export const SECTIONS = [
15
+ {
16
+ id: 'ai_tooling_landscape', name: 'AI Tooling Landscape', maxScore: 15, time: '~10 min',
17
+ questions: [
18
+ { id: 'q1_1', label: 'AI Tool Usage Overview', max: 5,
19
+ ask: 'Walk me through how your engineers use AI tools today — from IDE to deployment. What tools are in play, and how consistently are they used?',
20
+ listenFor: ['Specific tool names vs. vague answers', 'Standardization vs. individual choice', 'Whether tools span the full lifecycle', 'Shared configuration (team-wide settings, prompt libraries)'],
21
+ rubric: ['No AI tools in use', 'A few engineers use AI tools ad hoc', 'Multiple tools but no standardization', 'Standardized primary tool, some shared config', 'Standardized toolset covering multiple phases', 'Fully standardized and managed AI toolchain with usage tracking'] },
22
+ { id: 'q1_2', label: 'Tool Adoption Process', max: 5,
23
+ ask: 'How do you decide which AI tools to adopt? Is there a process, or does it happen organically?',
24
+ listenFor: ['Governance vs. grassroots adoption', 'Evaluation criteria (security, cost, effectiveness)', 'Budget ownership', 'Speed of adoption'],
25
+ rubric: ['No process; engineers install whatever', 'Informal process, no framework', 'Some evaluation criteria but inconsistent', 'Defined process with security review, but slow', 'Streamlined evaluation with clear criteria', 'Formal but fast governance with ongoing measurement'] },
26
+ { id: 'q1_3', label: 'Usage Measurement', max: 5,
27
+ ask: 'What percentage of your engineers use AI tools weekly? How do you know that number?',
28
+ listenFor: ['Actual data vs. guessing', 'Telemetry or license dashboards', 'Usage depth tracking', 'Awareness of adoption gaps'],
29
+ rubric: ['"I don\'t know" or clearly guessing', 'Rough guess based on anecdotes', 'Knows license count but not usage', 'Some usage data but not actively monitored', 'Actively tracks with team breakdowns', 'Real-time dashboards with usage depth and trends'] },
30
+ ],
31
+ },
32
+ {
33
+ id: 'dev_workflow_specs', name: 'Development Workflow & Specs', maxScore: 20, time: '~15 min',
34
+ questions: [
35
+ { id: 'q2_1', label: 'Feature Development Flow', max: 5,
36
+ ask: 'When a new feature comes in, what does the journey from idea to first PR look like? Walk me through a recent example.',
37
+ listenFor: ['Defined process or varies by person', 'Where AI enters the workflow', 'Handoff points and bottlenecks', 'Whether process is documented'],
38
+ rubric: ['No consistent process', 'Loose process, AI only during coding', 'Some features get specs inconsistently', 'Defined workflow with spec phase for major features', 'Consistent spec-first workflow with AI in coding and testing', 'Fully spec-driven with AI at every phase'] },
39
+ { id: 'q2_2', label: 'Spec Quality and Structure', max: 5,
40
+ ask: 'Do engineers write specs or design docs before coding? How structured are they?',
41
+ listenFor: ['Spec existence and consistency', 'Template usage and enforcement', 'Quality (vague vs. structured with ACs)', 'Whether specs live in the repo'],
42
+ rubric: ['No specs; code from tickets directly', 'Occasional design docs, no format', 'Specs exist but quality varies, no template', 'Template used for most features', 'Structured specs with enforcement and review', 'Rigorous spec process with ACs, constraints, AI-consumable format'] },
43
+ { id: 'q2_3', label: 'AI in the Design Phase', max: 5,
44
+ ask: 'How does AI participate in the design phase vs. just the coding phase? Is AI involved before the first line of code is written?',
45
+ listenFor: ['AI usage beyond code completion', 'Prompt engineering for design tasks', 'Whether specs feed into AI for implementation', 'Left-shift maturity'],
46
+ rubric: ['AI only for inline code completion', 'Code completion + occasional ChatGPT queries', 'Some engineers use AI for spec drafting', 'AI regularly used for specs and planning', 'AI integrated into design phase with structured prompts', 'AI across full design lifecycle: spec drafts, gap review, implementation plans'] },
47
+ { id: 'q2_4', label: 'AI Attribution and Traceability', max: 5,
48
+ ask: 'Look at your last 3 merged PRs. Can you tell which parts were AI-assisted?',
49
+ listenFor: ['Can they identify AI-assisted code at all', 'Commit trailers or metadata', 'PR descriptions mentioning AI', 'Automated tagging'],
50
+ rubric: ['Cannot tell which code is AI-assisted', 'Can guess from memory but no tracking', 'Some PRs mention AI inconsistently', 'Convention exists but not enforced', 'Consistent attribution via trailers, enforced', 'Automated attribution: tooling tags, searchable, auditable'] },
51
+ ],
52
+ },
53
+ {
54
+ id: 'cicd_quality', name: 'CI/CD & Quality', maxScore: 20, time: '~15 min',
55
+ questions: [
56
+ { id: 'q3_1', label: 'AI Validation in CI/CD', max: 5,
57
+ ask: 'Walk me through your CI/CD pipeline. Where does AI-generated code get validated differently from human-written code?',
58
+ listenFor: ['AI-specific validation steps', 'Eval gates', 'Bedrock Evaluations or similar', 'Security scanning for AI risks'],
59
+ rubric: ['Standard CI only, no AI-specific steps', 'Awareness but no action', 'Extra review for AI PRs but no automation', 'Some automated checks for AI code', 'Dedicated AI validation: eval gates, security scanning', 'Comprehensive: eval gates, Bedrock Evaluations, rollback triggers, feedback loops'] },
60
+ { id: 'q3_2', label: 'AI Bug Tracking', max: 5,
61
+ ask: 'Have you ever had an AI-generated bug reach production? What happened, and what did you learn?',
62
+ listenFor: ['Honesty and self-awareness', 'Whether they track AI-origin bugs separately', 'Post-mortem process', 'Process improvements from incidents'],
63
+ rubric: ['Don\'t track AI origin for bugs or denial', 'Aware of at least one bug, no tracking', 'Can describe incidents, response was ad hoc', 'AI bugs discussed in retros, some changes', 'AI bugs tagged in tracker, post-mortems address AI', 'Systematic tracking with defect attribution and feedback loops'] },
64
+ { id: 'q3_3', label: 'AI Code Quality Measurement', max: 5,
65
+ ask: 'How do you measure the quality of AI-generated code vs. human-written code? Is there a difference?',
66
+ listenFor: ['Whether they measure quality at all', 'Defect rate comparison', 'Acceptance rate tracking', 'Quality metrics with AI dimension'],
67
+ rubric: ['No systematic quality measurement', 'General metrics but no AI dimension', 'Anecdotal awareness, no measurement', 'Some metrics with AI awareness', 'Explicit AI vs. human quality comparison', 'Comprehensive: defect rates, review times, acceptance rates, dashboards'] },
68
+ { id: 'q3_4', label: 'Deployment Metrics and AI Impact', max: 5,
69
+ ask: 'What\'s your deployment frequency and lead time? How has AI affected these numbers?',
70
+ listenFor: ['DORA metrics awareness', 'Actual measurement', 'AI impact attribution', 'Before/after data'],
71
+ rubric: ['Don\'t track deployment metrics', 'Rough awareness, no formal tracking', 'Track frequency/lead time but no AI analysis', 'Track DORA, anecdotal AI impact', 'DORA with trend analysis and before/after data', 'Full DORA with AI-attributed impact analysis'] },
72
+ ],
73
+ },
74
+ {
75
+ id: 'metrics_visibility', name: 'Metrics & Visibility', maxScore: 15, time: '~10 min',
76
+ questions: [
77
+ { id: 'q4_1', label: 'Executive Visibility', max: 5,
78
+ ask: 'If your CTO asked right now, "What is AI doing for our engineering velocity?" — what would you show them?',
79
+ listenFor: ['Data vs. anecdotes', 'Dashboard existence and quality', 'Real-time vs. quarterly', 'Whether leadership actually asks'],
80
+ rubric: ['Nothing; would rely on anecdotes', 'License costs and adoption numbers only', 'Could assemble a deck with effort', 'Periodic report or dashboard, monthly/quarterly', 'Real-time dashboard with AI contribution metrics', 'Executive-ready dashboard with ROI, trends, automated reporting'] },
81
+ { id: 'q4_2', label: 'Engineering Metrics with AI Dimensions', max: 5,
82
+ ask: 'What engineering metrics do you currently track? Which ones include an AI dimension?',
83
+ listenFor: ['Baseline metrics maturity', 'AI dimensions on existing metrics', 'DORA, cycle time, throughput', 'Whether metrics drive decisions'],
84
+ rubric: ['Minimal or no engineering metrics', 'Basic metrics, no AI dimension', 'Standard metrics, no AI dimension', 'Good metrics + 1-2 AI-specific', 'Comprehensive with AI dimensions', 'Enhanced DORA with full AI dimensions, actively driving decisions'] },
85
+ { id: 'q4_3', label: 'AI ROI Reporting', max: 5,
86
+ ask: 'How do you report AI ROI to leadership? What\'s the cadence and what does it include?',
87
+ listenFor: ['Whether ROI is reported at all', 'Quantitative vs. qualitative', 'Cadence and audience', 'Cost + benefit included'],
88
+ rubric: ['No AI ROI reporting', 'Occasional informal updates', 'Periodic updates with some data', 'Quarterly with quantified metrics', 'Regular with quantified ROI and exec audience', 'Structured readouts with full ROI model, trends, forecasts'] },
89
+ ],
90
+ },
91
+ {
92
+ id: 'governance_security', name: 'Governance & Security', maxScore: 15, time: '~10 min',
93
+ questions: [
94
+ { id: 'q5_1', label: 'AI Guardrails', max: 5,
95
+ ask: 'What guardrails do you have around AI-generated code and AI agents? How do you limit what AI can do autonomously?',
96
+ listenFor: ['Whether guardrails exist at all', 'Specificity (vague vs. concrete rules)', 'Autonomy tiers', 'Agent-specific controls'],
97
+ rubric: ['No guardrails; AI has developer access', 'Informal guidance only', 'AI PRs require review but no formal policy', 'Documented guardrails with basic autonomy rules', 'Formal framework: autonomy tiers enforced by tooling', 'Comprehensive: tiers, sandboxing, restricted zones, audit trail'] },
98
+ { id: 'q5_2', label: 'AI Access and Permissions', max: 5,
99
+ ask: 'How do you handle AI access to sensitive data, credentials, or production systems? Does AI get the same access as the developer?',
100
+ listenFor: ['Scoped permissions vs. inherited access', 'IAM for AI agents', 'Credential management', 'Audit trails'],
101
+ rubric: ['AI has same access as developer, no audit', 'Awareness but no action', 'Basic controls (no prod access)', 'Scoped permissions, credential isolation, basic audit', 'Comprehensive: scoped IAM, audit trails, trust boundaries', 'Full governance: least-privilege, audit attribution, regular reviews'] },
102
+ { id: 'q5_3', label: 'AI Incident Response', max: 5,
103
+ ask: 'Do you have an AI-specific incident response process? If an AI agent causes a production issue, what happens?',
104
+ listenFor: ['AI-specific failure mode awareness', 'Runbooks or escalation paths', 'Post-mortem process for AI causes', 'Automated detection'],
105
+ rubric: ['No AI-specific incident response', 'Awareness but no specific process', 'Some ad hoc handling, not documented', 'AI considerations added to existing runbooks', 'Dedicated AI runbooks and escalation paths', 'Comprehensive: runbooks, automated detection, drills, feedback to guardrails'] },
106
+ ],
107
+ },
108
+ {
109
+ id: 'org_culture', name: 'Organization & Culture', maxScore: 15, time: '~10 min',
110
+ questions: [
111
+ { id: 'q6_1', label: 'AI Ownership and Sponsorship', max: 5,
112
+ ask: 'Who owns AI engineering transformation in your org? Is there a dedicated person, team, or budget?',
113
+ listenFor: ['Named individual or team', 'Executive sponsorship', 'Dedicated budget', 'Strategic intent vs. organic'],
114
+ rubric: ['Nobody owns it; grassroots only', 'Informal champion with no authority', 'Leadership supportive but no dedicated owner', 'Named owner with partial responsibility and budget', 'Dedicated owner with mandate, budget, exec backing', 'Named owner + team, C-level sponsorship, on company roadmap with OKRs'] },
115
+ { id: 'q6_2', label: 'AI Onboarding', max: 5,
116
+ ask: 'How do new engineers get onboarded to your AI toolchain? What does their first week look like with respect to AI tools?',
117
+ listenFor: ['Whether onboarding includes AI', 'Documentation and guides', 'Time-to-productivity', 'Ongoing training'],
118
+ rubric: ['AI not part of onboarding', 'Mentioned informally, no structured setup', 'Tools set up but no usage guidance', 'Structured: tools installed, usage guide, conventions', 'Comprehensive: codebase-specific tips, mentoring, first-week tasks', 'Full program: prompt libraries, benchmarks, ongoing training, feedback loop'] },
119
+ { id: 'q6_3', label: 'Blockers and Self-Awareness', max: 5,
120
+ ask: 'What\'s blocking you from getting more value from AI in engineering? If you could fix one thing tomorrow, what would it be?',
121
+ listenFor: ['Self-awareness and honesty', 'Specificity of blockers', 'Organizational vs. technical vs. cultural', 'Willingness to change'],
122
+ rubric: ['"Nothing, we\'re fine" or "AI isn\'t useful"', 'Vague blockers with no specifics', 'Specific blockers but no action taken', 'Specific blockers with some efforts underway', 'Clear gaps with prioritized action plan', 'Deep self-awareness with root cause analysis and evidence of iterating'] },
123
+ ],
124
+ },
125
+ ];
126
+ async function callBedrock(system, messages, modelId = 'us.anthropic.claude-sonnet-4-6', region = 'us-west-2') {
127
+ // Dynamic import so the CLI doesn't hard-fail if SDK isn't installed
128
+ const { BedrockRuntimeClient, InvokeModelCommand } = await import('@aws-sdk/client-bedrock-runtime');
129
+ const client = new BedrockRuntimeClient({ region });
130
+ const body = JSON.stringify({
131
+ anthropic_version: 'bedrock-2023-05-31',
132
+ max_tokens: 2048,
133
+ temperature: 0.3,
134
+ system,
135
+ messages,
136
+ });
137
+ const command = new InvokeModelCommand({
138
+ modelId,
139
+ contentType: 'application/json',
140
+ accept: 'application/json',
141
+ body: new TextEncoder().encode(body),
142
+ });
143
+ const response = await client.send(command);
144
+ const decoded = JSON.parse(new TextDecoder().decode(response.body));
145
+ return decoded.content?.[0]?.text ?? '';
146
+ }
147
+ export async function checkBedrockAccess(modelId = 'us.anthropic.claude-sonnet-4-6', region = 'us-west-2') {
148
+ try {
149
+ // Check SDK is installed
150
+ const { BedrockRuntimeClient, InvokeModelCommand } = await import('@aws-sdk/client-bedrock-runtime');
151
+ const client = new BedrockRuntimeClient({ region });
152
+ // Minimal request — 1 token max to keep it cheap and fast
153
+ const body = JSON.stringify({
154
+ anthropic_version: 'bedrock-2023-05-31',
155
+ max_tokens: 1,
156
+ temperature: 0,
157
+ messages: [{ role: 'user', content: 'hi' }],
158
+ });
159
+ const command = new InvokeModelCommand({
160
+ modelId,
161
+ contentType: 'application/json',
162
+ accept: 'application/json',
163
+ body: new TextEncoder().encode(body),
164
+ });
165
+ await client.send(command);
166
+ return { ok: true };
167
+ }
168
+ catch (err) {
169
+ const msg = err.message || String(err);
170
+ const name = err.name || '';
171
+ // SDK not installed
172
+ if (msg.includes('Cannot find module') || msg.includes('ERR_MODULE_NOT_FOUND')) {
173
+ return { ok: false, error: 'AWS SDK not installed. Run: npm install @aws-sdk/client-bedrock-runtime', errorType: 'sdk_missing' };
174
+ }
175
+ // No credentials configured
176
+ if (name === 'CredentialsProviderError' || msg.includes('Could not load credentials') || msg.includes('credential')) {
177
+ return { ok: false, error: 'No AWS credentials found.', errorType: 'no_credentials' };
178
+ }
179
+ // Access denied — model not enabled or wrong permissions
180
+ if (name === 'AccessDeniedException' || msg.includes('Access denied') || msg.includes('not authorized') || msg.includes('Legacy')) {
181
+ return { ok: false, error: msg, errorType: 'no_model_access' };
182
+ }
183
+ // Model not found in region
184
+ if (msg.includes('model identifier is invalid') || msg.includes('Could not resolve')) {
185
+ return { ok: false, error: msg, errorType: 'wrong_region' };
186
+ }
187
+ // Unknown error
188
+ return { ok: false, error: msg, errorType: 'unknown' };
189
+ }
190
+ }
191
+ // ---------------------------------------------------------------------------
192
+ // System prompts
193
+ // ---------------------------------------------------------------------------
194
+ function buildInterviewerSystemPrompt(scan, state) {
195
+ const section = SECTIONS[state.currentSectionIdx];
196
+ const question = section?.questions[state.currentQuestionIdx];
197
+ // Build scanner context for probing
198
+ const scannerContext = scan.categories
199
+ .map(c => {
200
+ const pct = c.maxPoints > 0 ? Math.round((c.earnedPoints / c.maxPoints) * 100) : 0;
201
+ return `- ${c.category}: ${c.earnedPoints}/${c.maxPoints} (${pct}%)`;
202
+ })
203
+ .join('\n');
204
+ const scannerGaps = scan.gaps?.length > 0
205
+ ? `\nScanner-detected gaps:\n${scan.gaps.map(g => `- ${g}`).join('\n')}`
206
+ : '';
207
+ let questionContext = '';
208
+ if (question) {
209
+ questionContext = `
210
+ CURRENT QUESTION: ${question.id} — ${question.label}
211
+ ASK: "${question.ask}"
212
+
213
+ WHAT TO LISTEN FOR:
214
+ ${question.listenFor.map(l => `- ${l}`).join('\n')}
215
+
216
+ SCORING RUBRIC (0-5):
217
+ ${question.rubric.map((r, i) => ` ${i}: ${r}`).join('\n')}
218
+
219
+ Follow-ups used so far for this question: ${state.followUpCount}/${state.maxFollowUps}
220
+ `;
221
+ }
222
+ // Use curated running context instead of full evidence dump
223
+ const contextSection = state.runningContext
224
+ ? `\nRELEVANT CONTEXT FROM PRIOR ANSWERS (use this to ask smarter questions and avoid re-asking what we already know):\n${state.runningContext}`
225
+ : '';
226
+ // Brief score summary for progress tracking
227
+ const scoreSummary = state.results.length > 0
228
+ ? `\nSCORES SO FAR: ${state.results.map(r => `${r.questionId}=${r.score}`).join(', ')}`
229
+ : '';
230
+ return `You are an expert AWS Solutions Architect conducting a PRISM D1 Velocity assessment interview. You are evaluating the AI-native software development lifecycle maturity of a startup.
231
+
232
+ ROLE: You are warm, conversational, and genuinely curious. This is a conversation, not an interrogation. You ask one question at a time, listen carefully, and use follow-up probes to dig deeper when answers are vague or surface-level.
233
+
234
+ RULES:
235
+ 1. Ask ONE question at a time. Never ask multiple questions in a single message.
236
+ 2. After the interviewee responds, decide: do you need a follow-up probe (max ${state.maxFollowUps} per question), or is the answer sufficient to score?
237
+ 3. When you have enough evidence to score, output a JSON scoring block at the END of your message (after your conversational response) in this exact format:
238
+ <!--SCORE:{"questionId":"${question?.id || ''}","score":N,"evidence":"brief summary of key evidence","notes":"any additional observations"}-->
239
+ 4. After scoring, naturally transition to the next question.
240
+ 5. Be conservative: when in doubt between two scores, pick the lower one.
241
+ 6. Reference scanner findings when relevant to probe discrepancies (e.g., "I noticed your repo doesn't have spec files — where do design decisions live?").
242
+ 7. Keep responses concise — 2-4 sentences for transitions, 1-2 sentences for follow-ups.
243
+ 8. Do NOT reveal the scoring rubric or your scores to the interviewee.
244
+ 9. USE CONTEXT from previous answers. If the interviewee already mentioned relevant details in an earlier answer, acknowledge that and build on it rather than asking them to repeat themselves. For example, if they already described their CI/CD pipeline, reference that when asking about AI validation in CI.
245
+ 10. If a previous answer partially addresses the current question, acknowledge what you already know and ask only about the gap.
246
+
247
+ SCANNER RESULTS (for context and probing):
248
+ Repository: ${scan.repoName}
249
+ Scanner Score: ${scan.totalScore}/${scan.maxScore}
250
+ ${scannerContext}
251
+ ${scannerGaps}
252
+
253
+ INTERVIEW PROGRESS:
254
+ Section ${state.currentSectionIdx + 1}/${SECTIONS.length}: ${section?.name || 'Complete'}
255
+ Question ${state.currentQuestionIdx + 1}/${section?.questions.length || 0} in this section
256
+ ${scoreSummary}
257
+ ${contextSection}
258
+
259
+ ${questionContext}
260
+
261
+ CUSTOMER INFO:
262
+ Name: ${state.customerName || 'Not yet collected'}
263
+ Team Size: ${state.teamSize || 'Unknown'}
264
+ Funding: ${state.fundingStage || 'Unknown'}`;
265
+ }
266
+ function buildScoringPrompt(question, conversation) {
267
+ // Extract just the Q&A for this question from the conversation
268
+ return `You are scoring an interview response. Based on the conversation below, assign a score from 0-5.
269
+
270
+ QUESTION: ${question.label}
271
+ "${question.ask}"
272
+
273
+ SCORING RUBRIC:
274
+ ${question.rubric.map((r, i) => ` ${i}: ${r}`).join('\n')}
275
+
276
+ WHAT TO LISTEN FOR:
277
+ ${question.listenFor.map(l => `- ${l}`).join('\n')}
278
+
279
+ CONVERSATION:
280
+ ${conversation.map(m => `${m.role.toUpperCase()}: ${m.content}`).join('\n\n')}
281
+
282
+ Respond with ONLY a JSON object:
283
+ {"score": N, "evidence": "brief summary of key evidence from their answer", "notes": "any observations"}`;
284
+ }
285
+ // ---------------------------------------------------------------------------
286
+ // Agent session management
287
+ // ---------------------------------------------------------------------------
288
+ export function createSession(scan) {
289
+ return {
290
+ phase: 'intro',
291
+ currentSectionIdx: 0,
292
+ currentQuestionIdx: 0,
293
+ followUpCount: 0,
294
+ maxFollowUps: 2,
295
+ results: [],
296
+ orgReadiness: {
297
+ executiveSponsor: false,
298
+ budgetAllocated: false,
299
+ dedicatedOwner: false,
300
+ awsRelationship: false,
301
+ appropriateTeamSize: false,
302
+ },
303
+ customerName: '',
304
+ saName: 'AI Interview Agent',
305
+ fundingStage: '',
306
+ teamSize: 0,
307
+ closingNotes: '',
308
+ messages: [],
309
+ scanData: scan,
310
+ runningContext: '',
311
+ };
312
+ }
313
+ export async function processMessage(state, userMessage, modelId, region) {
314
+ // Add user message to history
315
+ if (userMessage) {
316
+ state.messages.push({ role: 'user', content: userMessage });
317
+ }
318
+ // --- INTRO PHASE: collect basic info ---
319
+ if (state.phase === 'intro') {
320
+ return handleIntro(state, userMessage, modelId, region);
321
+ }
322
+ // --- INTERVIEW PHASE: ask questions, score responses ---
323
+ if (state.phase === 'interview') {
324
+ return handleInterview(state, userMessage, modelId, region);
325
+ }
326
+ // --- ORG READINESS PHASE ---
327
+ if (state.phase === 'org_readiness') {
328
+ return handleOrgReadiness(state, userMessage, modelId, region);
329
+ }
330
+ // --- CLOSING PHASE ---
331
+ if (state.phase === 'closing') {
332
+ return handleClosing(state, userMessage);
333
+ }
334
+ return { reply: 'Interview complete.', state, done: true };
335
+ }
336
+ async function handleIntro(state, userMessage, modelId, region) {
337
+ // First message — greet and ask for info
338
+ if (state.messages.length <= 1) {
339
+ const greeting = `Thanks for making time for this! I'm going to walk through how your team builds software today, with a focus on how AI tools fit into your workflow.
340
+
341
+ There are no wrong answers — we're trying to understand where you are so we can figure out the most useful next steps. I'll ask questions across six areas: AI tooling, development workflow, CI/CD, metrics, governance, and org structure.
342
+
343
+ Before we dive in, could you share:
344
+ 1. **Your name and role**
345
+ 2. **Company/team name**
346
+ 3. **Approximate engineering team size**
347
+ 4. **Funding stage** (Seed, Series A/B/C, etc.)`;
348
+ state.messages.push({ role: 'assistant', content: greeting });
349
+ return { reply: greeting, state, done: false };
350
+ }
351
+ // Parse intro info from the full conversation so far using the LLM
352
+ const allUserMessages = state.messages
353
+ .filter(m => m.role === 'user')
354
+ .map(m => m.content)
355
+ .join('\n');
356
+ const parsePrompt = `Extract the following from the user's messages. Return ONLY a JSON object with these exact keys:
357
+ - "customerName": the company or team name (string, or "" if not mentioned)
358
+ - "teamSize": number of engineers (number, or 0 if not mentioned)
359
+ - "fundingStage": funding stage like "Seed", "Series A", etc. (string, or "" if not mentioned)
360
+ - "intervieweeName": their personal name and role (string, or "" if not mentioned)
361
+
362
+ IMPORTANT: Use empty string "" for missing text fields and 0 for missing numbers. Never use null.
363
+
364
+ User messages:
365
+ ${allUserMessages}`;
366
+ try {
367
+ const raw = await callBedrock('You are a data extraction assistant. Return only valid JSON, no markdown.', [{ role: 'user', content: parsePrompt + '\n\nUser messages:\n' + allUserMessages }], modelId, region);
368
+ const cleaned = raw.replace(/```json?\n?/g, '').replace(/```/g, '').trim();
369
+ const info = JSON.parse(cleaned);
370
+ const name = String(info.customerName || '').trim();
371
+ const size = parseInt(String(info.teamSize || '0'), 10);
372
+ const funding = String(info.fundingStage || '').trim();
373
+ if (name && name !== 'null' && name !== 'Unknown')
374
+ state.customerName = name;
375
+ if (size > 0)
376
+ state.teamSize = size;
377
+ if (funding && funding !== 'null' && funding !== 'Unknown')
378
+ state.fundingStage = funding;
379
+ }
380
+ catch (err) {
381
+ // Log for debugging, then check what's missing below
382
+ console.error('Intro parse error:', err);
383
+ }
384
+ // Check what's still missing and ask follow-up
385
+ const missing = [];
386
+ if (!state.customerName)
387
+ missing.push('your **company or team name**');
388
+ if (!state.teamSize || state.teamSize === 0)
389
+ missing.push('your **approximate engineering team size**');
390
+ if (!state.fundingStage)
391
+ missing.push('your **funding stage** (Seed, Series A/B/C, etc.)');
392
+ // Also check for interviewee name — stored in messages but useful context
393
+ const hasName = state.messages.some(m => m.role === 'user') && !missing.length;
394
+ if (missing.length > 0) {
395
+ const followUp = `Thanks for that! I just want to make sure I have everything — could you also share ${missing.join(' and ')}?`;
396
+ state.messages.push({ role: 'assistant', content: followUp });
397
+ return { reply: followUp, state, done: false };
398
+ }
399
+ // All info collected — transition to interview
400
+ state.phase = 'interview';
401
+ state.currentSectionIdx = 0;
402
+ state.currentQuestionIdx = 0;
403
+ state.followUpCount = 0;
404
+ const section = SECTIONS[0];
405
+ const question = section.questions[0];
406
+ const transition = `Great, thanks for that context! Let's get started.
407
+
408
+ **Section 1: ${section.name}** (${section.time})
409
+
410
+ ${question.ask}`;
411
+ state.messages.push({ role: 'assistant', content: transition });
412
+ return { reply: transition, state, done: false };
413
+ }
414
+ async function handleInterview(state, userMessage, modelId, region) {
415
+ const section = SECTIONS[state.currentSectionIdx];
416
+ const question = section.questions[state.currentQuestionIdx];
417
+ // Build the system prompt and call the LLM
418
+ const systemPrompt = buildInterviewerSystemPrompt(state.scanData, state);
419
+ const bedrockMessages = state.messages.map(m => ({
420
+ role: m.role,
421
+ content: m.content,
422
+ }));
423
+ const reply = await callBedrock(systemPrompt, bedrockMessages, modelId, region);
424
+ // Check if the LLM included a score
425
+ const scoreMatch = reply.match(/<!--SCORE:(.*?)-->/s);
426
+ let cleanReply = reply.replace(/<!--SCORE:.*?-->/s, '').trim();
427
+ let scoredThisTurn = false;
428
+ if (scoreMatch) {
429
+ try {
430
+ const scoreData = JSON.parse(scoreMatch[1]);
431
+ state.results.push({
432
+ questionId: question.id,
433
+ label: question.label,
434
+ section: section.name,
435
+ score: Math.min(5, Math.max(0, scoreData.score)),
436
+ evidence: scoreData.evidence || '',
437
+ notes: scoreData.notes || '',
438
+ });
439
+ scoredThisTurn = true;
440
+ }
441
+ catch {
442
+ // If score parsing fails, use fallback scoring
443
+ const fallbackScore = await scoreFallback(question, state.messages, modelId, region, section.name);
444
+ state.results.push(fallbackScore);
445
+ scoredThisTurn = true;
446
+ }
447
+ // Advance to next question
448
+ state.followUpCount = 0;
449
+ state.currentQuestionIdx++;
450
+ // Check if section is complete
451
+ if (state.currentQuestionIdx >= section.questions.length) {
452
+ state.currentSectionIdx++;
453
+ state.currentQuestionIdx = 0;
454
+ // Check if all sections are complete
455
+ if (state.currentSectionIdx >= SECTIONS.length) {
456
+ state.phase = 'org_readiness';
457
+ cleanReply += `\n\nWe've covered all the interview questions. Just a few more quick items about organizational readiness.
458
+
459
+ Does your organization have an **executive sponsor** (CTO/VP level) who actively champions AI adoption in engineering?`;
460
+ }
461
+ else {
462
+ const nextSection = SECTIONS[state.currentSectionIdx];
463
+ cleanReply += `\n\n**Section ${state.currentSectionIdx + 1}: ${nextSection.name}** (${nextSection.time})`;
464
+ }
465
+ }
466
+ }
467
+ else {
468
+ // No score yet — this is a follow-up probe
469
+ state.followUpCount++;
470
+ // If we've hit max follow-ups, force a score on the next round
471
+ if (state.followUpCount >= state.maxFollowUps) {
472
+ // Score based on what we have
473
+ const fallbackScore = await scoreFallback(question, state.messages, modelId, region, section.name);
474
+ state.results.push(fallbackScore);
475
+ scoredThisTurn = true;
476
+ state.followUpCount = 0;
477
+ state.currentQuestionIdx++;
478
+ if (state.currentQuestionIdx >= section.questions.length) {
479
+ state.currentSectionIdx++;
480
+ state.currentQuestionIdx = 0;
481
+ if (state.currentSectionIdx >= SECTIONS.length) {
482
+ state.phase = 'org_readiness';
483
+ cleanReply += `\n\nGreat, we've covered all the interview questions. A few quick organizational readiness items now.
484
+
485
+ Does your organization have an **executive sponsor** (CTO/VP level) who actively champions AI adoption in engineering?`;
486
+ }
487
+ }
488
+ }
489
+ }
490
+ state.messages.push({ role: 'assistant', content: cleanReply });
491
+ // Refresh running context only when a question was scored this turn
492
+ if (scoredThisTurn && state.phase === 'interview') {
493
+ state.runningContext = await refreshRunningContext(state, modelId, region);
494
+ }
495
+ return { reply: cleanReply, state, done: false };
496
+ }
497
+ async function scoreFallback(question, messages, modelId, region, sectionName = '') {
498
+ try {
499
+ const prompt = buildScoringPrompt(question, messages.slice(-6));
500
+ const result = await callBedrock('You are a scoring assistant. Return only valid JSON.', [{ role: 'user', content: prompt }], modelId, region);
501
+ const parsed = JSON.parse(result.replace(/```json?\n?/g, '').replace(/```/g, '').trim());
502
+ return {
503
+ questionId: question.id,
504
+ label: question.label,
505
+ section: sectionName,
506
+ score: Math.min(5, Math.max(0, parsed.score || 0)),
507
+ evidence: parsed.evidence || '',
508
+ notes: parsed.notes || '',
509
+ };
510
+ }
511
+ catch {
512
+ return {
513
+ questionId: question.id,
514
+ label: question.label,
515
+ section: sectionName,
516
+ score: 0,
517
+ evidence: 'Could not score — insufficient evidence',
518
+ notes: 'Fallback scoring failed',
519
+ };
520
+ }
521
+ }
522
+ // ---------------------------------------------------------------------------
523
+ // Running context — curate relevant facts for remaining questions
524
+ // ---------------------------------------------------------------------------
525
+ async function refreshRunningContext(state, modelId, region) {
526
+ // Gather remaining question labels
527
+ const remaining = [];
528
+ for (let s = state.currentSectionIdx; s < SECTIONS.length; s++) {
529
+ const startQ = s === state.currentSectionIdx ? state.currentQuestionIdx : 0;
530
+ for (let q = startQ; q < SECTIONS[s].questions.length; q++) {
531
+ remaining.push(`${SECTIONS[s].name}: ${SECTIONS[s].questions[q].label}`);
532
+ }
533
+ }
534
+ if (remaining.length === 0 || state.results.length === 0)
535
+ return '';
536
+ // Build evidence from all scored questions
537
+ const evidence = state.results
538
+ .map(r => `[${r.section} — ${r.label}] Score: ${r.score}/5. ${r.evidence}${r.notes ? ' ' + r.notes : ''}`)
539
+ .join('\n');
540
+ const prompt = `You are helping an interviewer prepare context for upcoming questions.
541
+
542
+ SCORED ANSWERS SO FAR:
543
+ ${evidence}
544
+
545
+ REMAINING QUESTIONS (topics only):
546
+ ${remaining.map((r, i) => `${i + 1}. ${r}`).join('\n')}
547
+
548
+ Extract ONLY the facts from the scored answers that are directly relevant to the remaining questions. Be concise — short bullet points, no fluff. Drop anything that only mattered for already-scored questions. Max 10 bullets.
549
+
550
+ Format:
551
+ - fact 1
552
+ - fact 2`;
553
+ try {
554
+ const result = await callBedrock('You are a concise summarizer. Return only bullet points.', [{ role: 'user', content: prompt }], modelId, region);
555
+ return result.trim();
556
+ }
557
+ catch (err) {
558
+ console.error('Running context refresh failed:', err);
559
+ return state.runningContext; // keep previous context on failure
560
+ }
561
+ }
562
+ const ORG_READINESS_QUESTIONS = [
563
+ { key: 'executiveSponsor', label: 'Executive Sponsor', ask: 'Does your organization have an executive sponsor (CTO/VP level) who actively champions AI adoption in engineering?' },
564
+ { key: 'budgetAllocated', label: 'Budget Allocated', ask: 'Is there a dedicated budget allocated for AI tooling and transformation?' },
565
+ { key: 'dedicatedOwner', label: 'Dedicated Owner', ask: 'Is there a dedicated AI/platform team or named owner responsible for AI engineering transformation?' },
566
+ { key: 'awsRelationship', label: 'AWS Relationship', ask: 'Does your organization have an existing AWS commitment or relationship?' },
567
+ { key: 'appropriateTeamSize', label: 'Team Size', ask: 'Is your engineering team in the 20-200 engineer range?' },
568
+ ];
569
+ async function handleOrgReadiness(state, userMessage, modelId, region) {
570
+ // Parse yes/no from the user's response
571
+ const answeredCount = Object.values(state.orgReadiness).filter(v => v !== false).length;
572
+ // Actually count how many we've asked (based on conversation flow)
573
+ const orgAsked = state.messages.filter(m => m.role === 'assistant' && ORG_READINESS_QUESTIONS.some(q => m.content.includes(q.label) || m.content.includes(q.ask.substring(0, 30)))).length;
574
+ // Determine which question was just answered
575
+ const currentOrgIdx = Math.min(orgAsked - 1, ORG_READINESS_QUESTIONS.length - 1);
576
+ if (currentOrgIdx >= 0 && currentOrgIdx < ORG_READINESS_QUESTIONS.length) {
577
+ const currentQ = ORG_READINESS_QUESTIONS[currentOrgIdx];
578
+ // Use LLM to interpret yes/no
579
+ try {
580
+ const parseResult = await callBedrock(`The user was asked: "${currentQ.ask}". Based on their response, is the answer yes or no? Respond with ONLY "yes" or "no".`, [{ role: 'user', content: userMessage }], modelId, region);
581
+ state.orgReadiness[currentQ.key] = parseResult.trim().toLowerCase().startsWith('yes');
582
+ }
583
+ catch {
584
+ state.orgReadiness[currentQ.key] = userMessage.trim().toLowerCase().startsWith('y');
585
+ }
586
+ }
587
+ // Ask next org readiness question
588
+ const nextIdx = currentOrgIdx + 1;
589
+ if (nextIdx < ORG_READINESS_QUESTIONS.length) {
590
+ const nextQ = ORG_READINESS_QUESTIONS[nextIdx];
591
+ const reply = nextQ.ask;
592
+ state.messages.push({ role: 'assistant', content: reply });
593
+ return { reply, state, done: false };
594
+ }
595
+ // All org readiness done — move to closing
596
+ state.phase = 'closing';
597
+ const closingReply = `Thanks for those details! That covers everything I needed to ask.
598
+
599
+ A couple of wrap-up questions:
600
+ 1. Is there anything about your AI engineering practices that I didn't ask about but you think is important?
601
+ 2. What's the most impactful change you've made in the last quarter related to AI in engineering?`;
602
+ state.messages.push({ role: 'assistant', content: closingReply });
603
+ return { reply: closingReply, state, done: false };
604
+ }
605
+ async function handleClosing(state, userMessage) {
606
+ state.closingNotes = userMessage;
607
+ state.phase = 'complete';
608
+ const totalScore = state.results.reduce((sum, r) => sum + r.score, 0);
609
+ const reply = `Thank you so much for your time — this was really helpful. We'll compile everything into a detailed report.
610
+
611
+ **Interview complete!** Your responses have been scored across all 20 questions.
612
+
613
+ Click "View Report" below to see the full assessment results.`;
614
+ state.messages.push({ role: 'assistant', content: reply });
615
+ return { reply, state, done: true };
616
+ }
617
+ // ---------------------------------------------------------------------------
618
+ // Utility: convert agent results to the form data format expected by reportPage
619
+ // ---------------------------------------------------------------------------
620
+ export function agentResultsToFormData(state) {
621
+ const form = {
622
+ customerName: state.customerName || 'Unknown',
623
+ saName: state.saName,
624
+ fundingStage: state.fundingStage || '',
625
+ teamSize: String(state.teamSize || 10),
626
+ };
627
+ // Map question scores
628
+ for (const result of state.results) {
629
+ form[result.questionId] = String(result.score);
630
+ }
631
+ // Map section notes from evidence
632
+ for (const section of SECTIONS) {
633
+ const sectionResults = state.results.filter(r => section.questions.some(q => q.id === r.questionId));
634
+ const notes = sectionResults
635
+ .map(r => `${r.label}: ${r.evidence}`)
636
+ .join('; ');
637
+ form[`${section.id}_notes`] = notes;
638
+ }
639
+ // Org readiness
640
+ for (const [key, val] of Object.entries(state.orgReadiness)) {
641
+ if (val)
642
+ form[key] = 'on';
643
+ }
644
+ // Encode scan data
645
+ form.scanData = Buffer.from(JSON.stringify(state.scanData)).toString('base64');
646
+ return form;
647
+ }
648
+ //# sourceMappingURL=interview-agent.js.map