project-iris 0.0.7 → 0.0.11

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 (125) hide show
  1. package/README.md +294 -264
  2. package/dist/bridge/agent-runner.js +190 -0
  3. package/dist/bridge/connector-factory.js +4 -0
  4. package/dist/bridge/connectors/in-process-connector.js +29 -0
  5. package/dist/bridge/filesystem-connector.js +5 -0
  6. package/dist/cli.js +10 -2
  7. package/dist/commands/ask.js +150 -23
  8. package/dist/commands/bridge.js +8 -0
  9. package/dist/commands/flow.js +301 -0
  10. package/dist/commands/framework.js +273 -0
  11. package/dist/commands/generate.js +59 -0
  12. package/dist/commands/install.js +72 -29
  13. package/dist/commands/pack.js +7 -1
  14. package/dist/commands/run.js +195 -13
  15. package/dist/commands/status.js +9 -0
  16. package/dist/commands/uninstall.js +3 -1
  17. package/dist/commands/use.js +20 -0
  18. package/dist/commands/validate.js +80 -65
  19. package/dist/framework/framework-loader.js +97 -0
  20. package/dist/framework/framework-paths.js +48 -0
  21. package/dist/framework/framework-types.js +15 -0
  22. package/dist/iris/artifacts/config.js +68 -0
  23. package/dist/iris/artifacts/generator.js +88 -0
  24. package/dist/iris/artifacts/types.js +1 -0
  25. package/dist/iris/bundle.js +44 -0
  26. package/dist/iris/doctrine/collector.js +124 -0
  27. package/dist/iris/fixer.js +28 -22
  28. package/dist/iris/flows/manifest.js +124 -0
  29. package/dist/iris/framework-context.js +49 -0
  30. package/dist/iris/framework-manager.js +215 -0
  31. package/dist/iris/fs/atomic.js +22 -0
  32. package/dist/iris/importers/index.js +9 -0
  33. package/dist/iris/importers/types.js +8 -0
  34. package/dist/iris/importers/writer.js +139 -0
  35. package/dist/iris/installer.js +105 -40
  36. package/dist/iris/interactive/env.js +21 -0
  37. package/dist/iris/interactive/intent-interview.js +345 -0
  38. package/dist/iris/interactive/intent-schema.js +28 -0
  39. package/dist/iris/interactive/interview-io.js +22 -0
  40. package/dist/iris/interview/config.js +71 -0
  41. package/dist/iris/interview/types.js +16 -0
  42. package/dist/iris/interview/utils.js +38 -0
  43. package/dist/iris/packer.js +69 -47
  44. package/dist/iris/parsers/unit-parser.js +43 -0
  45. package/dist/iris/paths.js +18 -0
  46. package/dist/iris/policy.js +122 -17
  47. package/dist/iris/proc.js +56 -0
  48. package/dist/iris/resolver.js +3 -0
  49. package/dist/iris/routes.js +180 -11
  50. package/dist/iris/run-state.js +3 -0
  51. package/dist/iris/state.js +37 -9
  52. package/dist/iris/templates.js +70 -0
  53. package/dist/iris/tmp.js +24 -0
  54. package/dist/iris/uninstaller.js +24 -9
  55. package/dist/iris/utils/interpolate.js +42 -0
  56. package/dist/iris/validator.js +72 -10
  57. package/dist/iris/workflow/config.js +51 -0
  58. package/dist/iris/workflow/engine.js +129 -0
  59. package/dist/iris/workflow/steps.js +448 -0
  60. package/dist/iris/workflow/types.js +1 -0
  61. package/dist/utils/logo.js +17 -0
  62. package/dist/workflows/intent-inception.js +87 -65
  63. package/package.json +8 -6
  64. package/src/iris_bundle/.iris/aidlc/README.md +0 -16
  65. package/src/iris_bundle/.iris/aidlc/agents/iris-construction-agent.md +0 -35
  66. package/src/iris_bundle/.iris/aidlc/agents/iris-inception-agent.md +0 -30
  67. package/src/iris_bundle/.iris/aidlc/agents/iris-master-agent.md +0 -35
  68. package/src/iris_bundle/.iris/aidlc/agents/iris-operations-agent.md +0 -29
  69. package/src/iris_bundle/.iris/aidlc/commands/iris-construction-agent.md +0 -18
  70. package/src/iris_bundle/.iris/aidlc/commands/iris-inception-agent.md +0 -18
  71. package/src/iris_bundle/.iris/aidlc/commands/iris-master-agent.md +0 -18
  72. package/src/iris_bundle/.iris/aidlc/commands/iris-operations-agent.md +0 -18
  73. package/src/iris_bundle/.iris/aidlc/context/context-map.md +0 -25
  74. package/src/iris_bundle/.iris/aidlc/context/exclusion-rules.md +0 -13
  75. package/src/iris_bundle/.iris/aidlc/context/load-order.md +0 -25
  76. package/src/iris_bundle/.iris/aidlc/memory/intent-rules.md +0 -9
  77. package/src/iris_bundle/.iris/aidlc/memory/log-rules.md +0 -5
  78. package/src/iris_bundle/.iris/aidlc/memory/memory-bank.yaml +0 -39
  79. package/src/iris_bundle/.iris/aidlc/memory/unit-rules.md +0 -9
  80. package/src/iris_bundle/.iris/aidlc/quick-start.md +0 -24
  81. package/src/iris_bundle/.iris/aidlc/skills/execution/implementation.md +0 -14
  82. package/src/iris_bundle/.iris/aidlc/skills/execution/refactoring.md +0 -13
  83. package/src/iris_bundle/.iris/aidlc/skills/execution/scaffold-generation.md +0 -15
  84. package/src/iris_bundle/.iris/aidlc/skills/governance/escalation.md +0 -13
  85. package/src/iris_bundle/.iris/aidlc/skills/governance/quality-gates.md +0 -14
  86. package/src/iris_bundle/.iris/aidlc/skills/governance/stop-conditions.md +0 -11
  87. package/src/iris_bundle/.iris/aidlc/skills/reasoning/decomposition.md +0 -23
  88. package/src/iris_bundle/.iris/aidlc/skills/reasoning/risk-analysis.md +0 -14
  89. package/src/iris_bundle/.iris/aidlc/skills/reasoning/verification.md +0 -21
  90. package/src/iris_bundle/.iris/aidlc/standards/artifacts-registry.md +0 -38
  91. package/src/iris_bundle/.iris/aidlc/standards/decision-logging.md +0 -16
  92. package/src/iris_bundle/.iris/aidlc/standards/doctrine-structure.md +0 -31
  93. package/src/iris_bundle/.iris/aidlc/standards/documentation-rules.md +0 -15
  94. package/src/iris_bundle/.iris/aidlc/standards/file-structure.md +0 -21
  95. package/src/iris_bundle/.iris/aidlc/standards/naming-conventions.md +0 -18
  96. package/src/iris_bundle/.iris/aidlc/standards/phases-and-gates.md +0 -25
  97. package/src/iris_bundle/.iris/aidlc/standards/routes-and-routing.md +0 -35
  98. package/src/iris_bundle/.iris/aidlc/standards/tool-wrappers.md +0 -32
  99. package/src/iris_bundle/.iris/aidlc/templates/bolt.md +0 -23
  100. package/src/iris_bundle/.iris/aidlc/templates/doctrine-doc-template.md +0 -33
  101. package/src/iris_bundle/.iris/aidlc/templates/intent.md +0 -23
  102. package/src/iris_bundle/.iris/aidlc/templates/log.md +0 -24
  103. package/src/iris_bundle/.iris/aidlc/templates/review.md +0 -21
  104. package/src/iris_bundle/.iris/aidlc/templates/unit.md +0 -31
  105. package/src/iris_bundle/.iris/aidlc/validation/failure-modes.md +0 -16
  106. package/src/iris_bundle/.iris/aidlc/validation/phase-preconditions.md +0 -21
  107. package/src/iris_bundle/.iris/aidlc/validation/quality-checklist.md +0 -20
  108. package/src/iris_bundle/.iris/policy.yaml +0 -27
  109. package/src/iris_bundle/.iris/routes.yaml +0 -98
  110. package/src/iris_bundle/.iris/state.yaml +0 -7
  111. package/src/iris_bundle/.iris/tools/claude/.claude/claude.md +0 -9
  112. package/src/iris_bundle/.iris/tools/claude/.claude/commands/compare-specs.md +0 -203
  113. package/src/iris_bundle/.iris/tools/claude/.claude/commands/iris-construction-agent.md +0 -25
  114. package/src/iris_bundle/.iris/tools/claude/.claude/commands/iris-inception-agent.md +0 -25
  115. package/src/iris_bundle/.iris/tools/claude/.claude/commands/iris-master-agent.md +0 -25
  116. package/src/iris_bundle/.iris/tools/claude/.claude/commands/iris-operations-agent.md +0 -25
  117. package/src/iris_bundle/.iris/tools/codex/AGENTS.md +0 -15
  118. package/src/iris_bundle/.iris/tools/cursor/.cursor/commands/iris-construction-agent.md +0 -25
  119. package/src/iris_bundle/.iris/tools/cursor/.cursor/commands/iris-inception-agent.md +0 -25
  120. package/src/iris_bundle/.iris/tools/cursor/.cursor/commands/iris-master-agent.md +0 -25
  121. package/src/iris_bundle/.iris/tools/cursor/.cursor/commands/iris-operations-agent.md +0 -25
  122. package/src/iris_bundle/.iris/tools/gemini/.gemini/commands/iris-construction-agent.toml +0 -29
  123. package/src/iris_bundle/.iris/tools/gemini/.gemini/commands/iris-inception-agent.toml +0 -29
  124. package/src/iris_bundle/.iris/tools/gemini/.gemini/commands/iris-master-agent.toml +0 -29
  125. package/src/iris_bundle/.iris/tools/gemini/.gemini/commands/iris-operations-agent.toml +0 -29
@@ -4,8 +4,9 @@ import kleur from "kleur";
4
4
  import inquirer from "inquirer";
5
5
  import { ensureDir, sanitizeFolderName, spawnAsync } from "../lib.js";
6
6
  import { updateManifest } from "./manifest.js";
7
- export const TOOLS = ["claude", "cursor", "gemini", "antigravity", "codex"];
8
- const BUNDLE_ROOT = path.resolve(path.dirname(new URL(import.meta.url).pathname), "../../src/iris_bundle");
7
+ import { getBundleRoot, getBundledFrameworksDir } from "./bundle.js";
8
+ export const TOOLS = ["claude", "cursor", "gemini", "antigravity", "codex", "auto"];
9
+ const BUNDLE_ROOT = getBundleRoot();
9
10
  /**
10
11
  * Ensure package.json exists in the target directory.
11
12
  * If missing, create a minimal one.
@@ -63,10 +64,11 @@ export function detectTools(root) {
63
64
  gemini: fs.existsSync(path.join(root, ".gemini")),
64
65
  antigravity: fs.existsSync(path.join(root, ".antigravity")),
65
66
  codex: fs.existsSync(path.join(root, "AGENTS.md")),
67
+ auto: false,
66
68
  };
67
69
  return TOOLS.filter(t => checks[t]);
68
70
  }
69
- export async function installIris(root, tools) {
71
+ export async function installIris(root, tools, options = {}) {
70
72
  // Step 0: Ensure package.json exists (for empty folder support)
71
73
  ensurePackageJson(root);
72
74
  const installedPaths = [];
@@ -79,49 +81,112 @@ export async function installIris(root, tools) {
79
81
  const targetIris = path.join(root, ".iris");
80
82
  const targetDoctrine = path.join(targetIris, "aidlc");
81
83
  const targetTools = path.join(targetIris, "tools");
82
- // Step C: Install IRIS core
83
- ensureDir(targetIris);
84
- // 1. Doctrine
85
- let backupPath = "";
86
- if (fs.existsSync(targetDoctrine)) {
87
- const { overwrite } = await inquirer.prompt([
88
- {
89
- type: "confirm",
90
- name: "overwrite",
91
- message: "IRIS doctrine (.iris/aidlc) already exists. Overwrite with fresh bundle?",
92
- default: false
84
+ // Step C: Framework & Core Configuration
85
+ // New (Step 3): Install iris-core framework as the Single Source of Truth
86
+ // Then populate legacy mirrors (.iris/aidlc, policy.yaml, etc) from it.
87
+ const bundledFrameworksDir = getBundledFrameworksDir();
88
+ const sourceIrisCore = path.join(bundledFrameworksDir, "iris-core");
89
+ const frameworksDir = path.join(targetIris, "frameworks");
90
+ const targetIrisCore = path.join(frameworksDir, "iris-core");
91
+ ensureDir(frameworksDir);
92
+ // 1. Install Framework (Idempotent: Skip if exists unless --force)
93
+ let installedFramework = false;
94
+ if (fs.existsSync(sourceIrisCore)) {
95
+ if (fs.existsSync(targetIrisCore) && !options.force) {
96
+ // Skip
97
+ // console.log(kleur.gray("Skipped iris-core (exists)"));
98
+ // Check for version mismatch (UX enhancement)
99
+ try {
100
+ const targetManifestPath = path.join(targetIrisCore, "framework.yaml");
101
+ const sourceManifestPath = path.join(sourceIrisCore, "framework.yaml");
102
+ if (fs.existsSync(targetManifestPath) && fs.existsSync(sourceManifestPath)) {
103
+ const targetManifest = JSON.parse(fs.readFileSync(targetManifestPath, 'utf8')); // Yaml is subset of json? No, need yaml parser or simple regex for version.
104
+ // We don't have yaml parser imported here easily (maybe?) inquirer or kleur doesn't help.
105
+ // Let's use simple regex since we control the manifest format mostly.
106
+ const getVer = (content) => {
107
+ const m = content.match(/version:\s*["']?([\w.-]+)["']?/);
108
+ return m ? m[1] : null;
109
+ };
110
+ const targetVer = getVer(fs.readFileSync(targetManifestPath, 'utf8'));
111
+ const sourceVer = getVer(fs.readFileSync(sourceManifestPath, 'utf8'));
112
+ if (targetVer && sourceVer && targetVer !== sourceVer) {
113
+ console.log(kleur.yellow(`Warning: Installed iris-core is v${targetVer}, but bundled is v${sourceVer}.`));
114
+ console.log(kleur.yellow(`Run 'iris install --force' to update (will overwrite changes).`));
115
+ }
116
+ }
117
+ }
118
+ catch (e) {
119
+ // ignore
120
+ }
121
+ }
122
+ else {
123
+ // Install / Overwrite
124
+ if (fs.existsSync(targetIrisCore)) {
125
+ // Force overwrite implies we can clobber.
126
+ // Simple recursive copy over top works for updating files.
93
127
  }
94
- ]);
95
- if (overwrite) {
96
- backupPath = `${targetDoctrine}.backup.${Date.now()}`;
97
- fs.renameSync(targetDoctrine, backupPath);
98
- console.log(kleur.yellow(`Backed up existing doctrine to ${path.relative(root, backupPath)}`));
99
- copyDirRec(bundleDoctrine, targetDoctrine, installedPaths, root); // copy new
128
+ ensureDir(targetIrisCore);
129
+ copyDirRec(sourceIrisCore, targetIrisCore, [], root, true);
130
+ installedFramework = true;
131
+ installedPaths.push({
132
+ path: ".iris/frameworks/iris-core",
133
+ type: "directory",
134
+ status: "installed"
135
+ });
100
136
  }
101
- // else keep existing
102
137
  }
103
- else {
104
- copyDirRec(bundleDoctrine, targetDoctrine, installedPaths, root);
138
+ // 2. Compatibility Mirror: .iris/aidlc
139
+ // Populated FROM the valid target iris-core framework
140
+ // This ensures legacy tools see the same templates as the framework.
141
+ const targetAidlc = path.join(targetIris, "aidlc");
142
+ ensureDir(targetAidlc);
143
+ // Mirror Templates
144
+ const frameworkTemplates = path.join(targetIrisCore, "templates");
145
+ const aidlcTemplates = path.join(targetAidlc, "templates");
146
+ // Only mirror if framework templates exist
147
+ if (fs.existsSync(frameworkTemplates)) {
148
+ // If aidlc/templates exists, only overwrite if force?
149
+ // Or always align mirror to framework?
150
+ // "Keep it read-only in spirit". Aligning is safer.
151
+ // But if user modified aidlc, we clobber?
152
+ // Let's protect user edits in aidlc too unless force.
153
+ if (!fs.existsSync(aidlcTemplates) || options.force) {
154
+ ensureDir(aidlcTemplates);
155
+ copyDirRec(frameworkTemplates, aidlcTemplates, [], root, true);
156
+ }
157
+ }
158
+ // Legacy Config Mirrors (policy.yaml, routes.yaml)
159
+ // Copy from framework -> .iris/ root IF MISSING (don't overwrite config by default)
160
+ // We do NOT use --force to overwrite these configs usually, as they are user-owned once installed.
161
+ // Spec: "Default: Skip if exists (preserve user edits). If --force: Overwrite."
162
+ // So we respect that for these files too if we consider them part of "install".
163
+ const frameworkPolicy = path.join(targetIrisCore, "policy.yaml");
164
+ const targetPolicy = path.join(targetIris, "policy.yaml");
165
+ if (fs.existsSync(frameworkPolicy) && (!fs.existsSync(targetPolicy) || options.force)) {
166
+ fs.copyFileSync(frameworkPolicy, targetPolicy);
167
+ installedPaths.push({ path: ".iris/policy.yaml", type: "file", status: "installed" });
168
+ }
169
+ const frameworkRoutes = path.join(targetIrisCore, "routes.yaml");
170
+ const targetRoutes = path.join(targetIris, "routes.yaml");
171
+ if (fs.existsSync(frameworkRoutes) && (!fs.existsSync(targetRoutes) || options.force)) {
172
+ fs.copyFileSync(frameworkRoutes, targetRoutes);
173
+ installedPaths.push({ path: ".iris/routes.yaml", type: "file", status: "installed" });
174
+ }
175
+ // Add README to aidlc
176
+ const mirrorReadme = path.join(targetAidlc, "README.md");
177
+ if (!fs.existsSync(mirrorReadme)) {
178
+ fs.writeFileSync(mirrorReadme, "# Legacy Mirror\n\nThis directory is a compatibility mirror of `.iris/frameworks/iris-core`.\nDo not edit files here; they may be overwritten by system updates.\nEdit the framework files instead.\n");
105
179
  }
106
- // 2. Policy & Routes (Overwrite or keep? Usually overwrite core config if missing, but maybe prompt if exists?)
107
- // Spec says: "Install .iris/policy.yaml... .iris/routes.yaml"
108
- // Let's safe install: only if missing? Or overwrite?
109
- // Spec doesn't explicitly say prompt for policy.
110
- // "Install IRIS core... .iris/policy.yaml"
111
- // Let's assume non-destructive for config if it exists, to preserve user edits.
112
- safeCopy(path.join(bundleIris, "policy.yaml"), path.join(targetIris, "policy.yaml"), installedPaths, root);
113
- safeCopy(path.join(bundleIris, "routes.yaml"), path.join(targetIris, "routes.yaml"), installedPaths, root);
114
180
  // 3. State (only if missing)
115
181
  if (!fs.existsSync(path.join(targetIris, "state.yaml"))) {
116
- fs.copyFileSync(path.join(bundleIris, "state.yaml"), path.join(targetIris, "state.yaml"));
117
- // Don't track state in manifest as "installed" usually, as it changes?
118
- // Or do track it so we know we created it? "remove only paths that were installed"
119
- // If we track it, uninstall might delete it. Good.
120
- installedPaths.push({
121
- path: ".iris/state.yaml",
122
- type: "file",
123
- status: "installed"
124
- });
182
+ if (fs.existsSync(path.join(bundleIris, "state.yaml"))) {
183
+ fs.copyFileSync(path.join(bundleIris, "state.yaml"), path.join(targetIris, "state.yaml"));
184
+ installedPaths.push({
185
+ path: ".iris/state.yaml",
186
+ type: "file",
187
+ status: "installed"
188
+ });
189
+ }
125
190
  }
126
191
  // Step D: Memory Bank Skeleton
127
192
  const memoryDirs = ["intents", "units", "bolts", "standards", "logs"];
@@ -0,0 +1,21 @@
1
+ export function checkInteractiveMode(options) {
2
+ // 1. Force Interactive overrides all
3
+ if (options.forceInteractive) {
4
+ return { isInteractive: true, reason: "Forced by user flag" };
5
+ }
6
+ // 2. Explicit Non-Interactive
7
+ if (options.nonInteractive) {
8
+ return { isInteractive: false, reason: "User requested non-interactive mode" };
9
+ }
10
+ // 3. CI Detection
11
+ // Common CI env vars: CI, GITHUB_ACTIONS, GITLAB_CI, CIRCLECI, TRAVIS
12
+ if (process.env.CI || process.env.GITHUB_ACTIONS || process.env.GITLAB_CI) {
13
+ return { isInteractive: false, reason: "CI environment detected" };
14
+ }
15
+ // 4. TTY Detection
16
+ // process.stdin.isTTY is undefined or false if piped/redirected
17
+ if (!process.stdin.isTTY) {
18
+ return { isInteractive: false, reason: "No TTY detected (piped input)" };
19
+ }
20
+ return { isInteractive: true };
21
+ }
@@ -0,0 +1,345 @@
1
+ import kleur from "kleur";
2
+ import fs from "fs";
3
+ import { writeJsonAtomic } from "../fs/atomic.js";
4
+ import { InquirerIO } from "./interview-io.js";
5
+ import { ensureDir } from "../../lib.js";
6
+ import { getInboxPath, getHistoryDraftPath, getLatestDraftPath, getLegacyDraftPath } from "../paths.js";
7
+ import { createEmptyDraft, validateDraft } from "./intent-schema.js";
8
+ // --- Rubric Logic Moved to Schema/Shared or kept here? ---
9
+ // The prompt asked for logic in schema.ts? No, prompt asked for SCHEMA definition in schema.ts.
10
+ // Let's keep logic in schema.ts or here.
11
+ // Actually, `calculateConfidence` is better in `intent-schema.ts` if we want to test it easily without inquirer deps.
12
+ // But my previous `intent-schema.ts` didn't have `calculateConfidence`.
13
+ // I should probably move `calculateConfidence` into `intent-schema.ts` OR just import the type and keep logic here.
14
+ // To minimize changes to `intent-schema.ts` which I already wrote, I will keep `calculateConfidence` here
15
+ // BUT I need to make sure I use the types correctly.
16
+ // WAIT, I ALREADY WROTE `intent-schema.ts` WITHOUT `calculateConfidence`.
17
+ // So I will implement `calculateConfidence` here in this file, extending the one in schema if needed or just standalone.
18
+ // Re-defining locally to match the file I tried to write before
19
+ // ACTUALLY, checking `intent-schema.ts` content from Step 193... it does NOT have calculateConfidence.
20
+ // So I must implement it here.
21
+ export function formatIntentDraft(draft) {
22
+ return `
23
+ # Intent: ${draft.goal}
24
+
25
+ ## Context
26
+ - **Target User:** ${draft.userType}
27
+ - **Project Phase:** ${draft.projectPhase}
28
+ - **Confidence:** ${draft.confidence.score}
29
+
30
+ ## Success Criteria
31
+ ${draft.successCriteria.length > 0 ? draft.successCriteria.map(c => `- ${c}`).join("\n") : "(None provided)"}
32
+
33
+ ## Technical Constraints
34
+ ${draft.constraints.map(c => `- ${c}`).join("\n")}
35
+
36
+ ## Tooling Stack
37
+ ${draft.tools.map(t => `- ${t}`).join("\n")}
38
+
39
+ ## Additional Details
40
+ ${draft.nonGoals.length > 0 ? `\n### Non-Goals\n${draft.nonGoals.map(x => `- ${x}`).join("\n")}` : ""}
41
+ ${draft.acceptanceTests.length > 0 ? `\n### Acceptance Tests\n${draft.acceptanceTests.map(x => `- ${x}`).join("\n")}` : ""}
42
+
43
+ ---
44
+ *Generated via IRIS Interactive Interview*
45
+ `.trim();
46
+ }
47
+ function calcConfidenceScore(draft) {
48
+ let score = 0;
49
+ const explanation = [];
50
+ // 1. Goal Clarity
51
+ if (draft.goal && draft.goal.split(" ").length > 3) {
52
+ score += 0.2;
53
+ explanation.push("+0.2 for clear goal statement");
54
+ }
55
+ else if (draft.goal) {
56
+ score += 0.1;
57
+ explanation.push("+0.1 for goal presence");
58
+ }
59
+ const successLen = draft.successCriteria?.length || 0;
60
+ if (successLen > 0) {
61
+ score += 0.2;
62
+ explanation.push(`+0.2 for ${successLen} success criteria`);
63
+ }
64
+ const acceptLen = draft.acceptanceTests?.length || 0;
65
+ if (acceptLen > 0) {
66
+ score += 0.1;
67
+ explanation.push(`+0.1 for ${acceptLen} acceptance tests`);
68
+ }
69
+ if (draft.constraints && draft.constraints.length > 0) {
70
+ score += 0.1;
71
+ explanation.push("+0.1 for identified constraints");
72
+ }
73
+ if (draft.tools && draft.tools.length > 0) {
74
+ score += 0.1;
75
+ explanation.push("+0.1 for tool selection");
76
+ }
77
+ if (draft.nonGoals && draft.nonGoals.length > 0) {
78
+ score += 0.1;
79
+ explanation.push("+0.1 for explicit non-goals");
80
+ }
81
+ if (draft.userType && draft.projectPhase) {
82
+ score += 0.1;
83
+ explanation.push("+0.1 for user context & phase");
84
+ }
85
+ score = Math.min(1.0, Math.max(0, Math.round(score * 10) / 10));
86
+ return { score, explanation };
87
+ }
88
+ // Export for tests if needed, but the main export is runIntentInterview
89
+ export { calcConfidenceScore as calculateConfidence };
90
+ // --- Interview Logic ---
91
+ export async function runIntentInterview(initialIntent, config, existingDraft, io = new InquirerIO()) {
92
+ io.header("\n🎤 Interactive Intent Interview");
93
+ if (config?.guidance?.systemNotes) {
94
+ io.print(kleur.dim(config.guidance.systemNotes));
95
+ }
96
+ else {
97
+ io.print(kleur.dim("Let's clarify your goals before we start building.\n"));
98
+ }
99
+ const draftPartial = existingDraft ? { ...existingDraft } : createEmptyDraft();
100
+ // If we have an existing draft, we assume we are resuming/refining, so we don't overwrite unless intent is new?
101
+ // Actually if initialIntent is provided on resume, it might be the "follow up prompt" context, which isn't a draft field.
102
+ // But draftPartial.goal is the high level goal.
103
+ if (!existingDraft && initialIntent) {
104
+ draftPartial.goal = initialIntent;
105
+ }
106
+ // 1. Goal (Skip if exists)
107
+ if (!draftPartial.goal) {
108
+ const { goal } = await io.ask([
109
+ {
110
+ type: "input",
111
+ name: "goal",
112
+ message: config?.guidance?.starterQuestions?.[0] || "What are you trying to build?",
113
+ validate: (input) => input.trim().length > 0 || "Please provide a goal."
114
+ }
115
+ ]);
116
+ draftPartial.goal = goal;
117
+ }
118
+ else {
119
+ // If resuming or passed in arg
120
+ io.print(`${kleur.cyan("? ")}Goal: ${kleur.cyan(draftPartial.goal || "")}`);
121
+ }
122
+ // 2. User & Phase
123
+ const context = await io.ask([
124
+ {
125
+ type: "list",
126
+ name: "userType",
127
+ message: "Who is the primary user?",
128
+ choices: [
129
+ "Individual Developer",
130
+ "Team / Connectors",
131
+ "Enterprise / Organization",
132
+ "End Consumers"
133
+ ]
134
+ },
135
+ {
136
+ type: "list",
137
+ name: "projectPhase",
138
+ message: "What phase is this project in?",
139
+ choices: [
140
+ { name: "Inception (New Idea)", value: "Inception" },
141
+ { name: "Construction (Active Dev)", value: "Construction" },
142
+ { name: "Operations (Maintenance)", value: "Operations" }
143
+ ]
144
+ }
145
+ ]);
146
+ Object.assign(draftPartial, context);
147
+ // 3. Success Criteria (Required)
148
+ io.print("\n" + kleur.bold("Success Criteria"));
149
+ // Loop until we have at least one criteria (Enforcement)
150
+ while (true) {
151
+ io.print(kleur.dim("What must be true for this to be considered done? (Enter empty line to finish)"));
152
+ const criteria = [];
153
+ if (draftPartial.successCriteria)
154
+ criteria.push(...draftPartial.successCriteria);
155
+ while (true) {
156
+ const { item } = await io.ask([{
157
+ type: "input",
158
+ name: "item",
159
+ message: `Criterion ${criteria.length + 1}:`
160
+ }]);
161
+ if (!item.trim())
162
+ break;
163
+ criteria.push(item.trim());
164
+ }
165
+ draftPartial.successCriteria = criteria;
166
+ if (criteria.length === 0) {
167
+ io.warn("⚠ At least one Success Criterion is required.");
168
+ const { retry } = await io.ask([{
169
+ type: "confirm",
170
+ name: "retry",
171
+ message: "Try adding criteria again?",
172
+ default: true
173
+ }]);
174
+ if (!retry) {
175
+ io.warn("Cannot proceed without success criteria. Interview cancelled.");
176
+ return null;
177
+ }
178
+ }
179
+ else {
180
+ break;
181
+ }
182
+ }
183
+ // 4. Constraints (Multi-select)
184
+ const { constraints } = await io.ask([
185
+ {
186
+ type: "checkbox",
187
+ name: "constraints",
188
+ message: "What constraints matter most?",
189
+ choices: [
190
+ "Time (Speed to Market)",
191
+ "Quality (Robustness)",
192
+ "Compliance / Security",
193
+ "Performance",
194
+ "Cost / Budget",
195
+ "Simplicity"
196
+ ]
197
+ }
198
+ ]);
199
+ draftPartial.constraints = constraints;
200
+ // 5. Tools
201
+ const { tools } = await io.ask([
202
+ {
203
+ type: "checkbox",
204
+ name: "tools",
205
+ message: "Which tools/platforms are involved?",
206
+ choices: [
207
+ { name: "Node.js / TypeScript", checked: true },
208
+ "Python",
209
+ "React / Frontend",
210
+ "Docker / K8s",
211
+ "AWS / Cloud",
212
+ "Database (SQL/NoSQL)"
213
+ ]
214
+ }
215
+ ]);
216
+ draftPartial.tools = tools;
217
+ // 6. Optional Fields
218
+ const { addMore } = await io.ask([{
219
+ type: "confirm",
220
+ name: "addMore",
221
+ message: "Add non-goals, risks, or acceptance tests? (Optional)",
222
+ default: false
223
+ }]);
224
+ if (addMore) {
225
+ // Non Goals
226
+ io.print("\n" + kleur.bold("Non-Goals (What are we NOT doing?)"));
227
+ while (true) {
228
+ const { item } = await io.ask([{ type: "input", name: "item", message: ">" }]);
229
+ if (!item.trim())
230
+ break;
231
+ if (!draftPartial.nonGoals)
232
+ draftPartial.nonGoals = [];
233
+ draftPartial.nonGoals.push(item.trim());
234
+ }
235
+ // Acceptance Tests
236
+ io.print("\n" + kleur.bold("Acceptance Tests (How will we verify?)"));
237
+ while (true) {
238
+ const { item } = await io.ask([{ type: "input", name: "item", message: ">" }]);
239
+ if (!item.trim())
240
+ break;
241
+ if (!draftPartial.acceptanceTests)
242
+ draftPartial.acceptanceTests = [];
243
+ draftPartial.acceptanceTests.push(item.trim());
244
+ }
245
+ }
246
+ // Calculate Confidence
247
+ const rubric = calcConfidenceScore(draftPartial);
248
+ // Assemble final draft
249
+ const draft = {
250
+ ...createEmptyDraft(),
251
+ ...draftPartial,
252
+ confidence: rubric,
253
+ // Ensure successCriteria is array
254
+ successCriteria: draftPartial.successCriteria || [],
255
+ createdAt: new Date().toISOString()
256
+ };
257
+ // Summary
258
+ io.divider();
259
+ io.header("📋 Intent Summary");
260
+ io.print(`Goal: ${draft.goal}`);
261
+ io.print(`Context: ${draft.userType} / ${draft.projectPhase}`);
262
+ io.print(`Criteria: ${draft.successCriteria.length} items`);
263
+ io.print(`Tools: ${draft.tools.join(", ")}`);
264
+ io.print("");
265
+ const scoreColor = draft.confidence.score > 0.7 ? kleur.green : draft.confidence.score > 0.4 ? kleur.yellow : kleur.red;
266
+ io.print(`Confidence Score: ${scoreColor(draft.confidence.score.toFixed(2) + " / 1.0")}`);
267
+ draft.confidence.explanation.forEach(exp => io.print(kleur.dim(` ${exp}`)));
268
+ io.divider();
269
+ // Validation check
270
+ const val = validateDraft(draft);
271
+ if (!val.valid) {
272
+ io.warn(kleur.red("Draft validation failed:"));
273
+ val.errors.forEach(e => io.print(`- ${e}`));
274
+ io.print("Please restart or fix inputs.");
275
+ return null;
276
+ }
277
+ const { confirm } = await io.ask([
278
+ {
279
+ type: "confirm",
280
+ name: "confirm",
281
+ message: "Do you want to generate intent artifacts now?",
282
+ default: true
283
+ }
284
+ ]);
285
+ if (!confirm) {
286
+ io.warn("Interview cancelled. No artifacts generated.");
287
+ return null;
288
+ }
289
+ return draft;
290
+ }
291
+ /**
292
+ * Saves intent draft enforcing schema.
293
+ * @param writeLegacy If true, also writes .iris/inbox/intent-draft.json (deprecated)
294
+ */
295
+ export function saveIntentDraft(root, draft, writeLegacy = false) {
296
+ const inboxDir = getInboxPath(root);
297
+ ensureDir(inboxDir);
298
+ // Enforce Schema Defaults & Versioning
299
+ const fullDraft = {
300
+ ...createEmptyDraft(),
301
+ ...draft,
302
+ createdAt: draft.createdAt || new Date().toISOString()
303
+ };
304
+ // Recalculate confidence if missing
305
+ if (!draft.confidence) {
306
+ fullDraft.confidence = calcConfidenceScore(fullDraft);
307
+ }
308
+ // 1. Save History
309
+ const historyFile = getHistoryDraftPath(root, new Date(fullDraft.createdAt));
310
+ // 2. Save Latest (Canonical)
311
+ const latestFile = getLatestDraftPath(root);
312
+ writeJsonAtomic(historyFile, fullDraft);
313
+ writeJsonAtomic(latestFile, fullDraft);
314
+ // 3. Optional Legacy
315
+ if (writeLegacy) {
316
+ const legacyFile = getLegacyDraftPath(root);
317
+ writeJsonAtomic(legacyFile, fullDraft);
318
+ }
319
+ }
320
+ /**
321
+ * Migrates legacy intent-draft.json to intent-draft.latest.json.
322
+ */
323
+ export function migrateLegacyDraft(root) {
324
+ const legacyFile = getLegacyDraftPath(root);
325
+ const latestFile = getLatestDraftPath(root);
326
+ if (fs.existsSync(legacyFile) && !fs.existsSync(latestFile)) {
327
+ try {
328
+ const content = JSON.parse(fs.readFileSync(legacyFile, "utf-8"));
329
+ // Enforce schema by saving via normalized function
330
+ // Treat content as partial
331
+ if (!content.successCriteria) {
332
+ content.successCriteria = ["(Migrated from legacy draft)"];
333
+ }
334
+ // Re-save (generates latest + history)
335
+ // Do NOT write legacy again (avoid loop), so writeLegacy=false
336
+ saveIntentDraft(root, content, false);
337
+ return true;
338
+ }
339
+ catch (e) {
340
+ console.error(kleur.red(`Failed to migrate legacy draft: ${e}`));
341
+ return false;
342
+ }
343
+ }
344
+ return false;
345
+ }
@@ -0,0 +1,28 @@
1
+ export const CURRENT_SCHEMA_VERSION = "1";
2
+ export function createEmptyDraft() {
3
+ return {
4
+ version: CURRENT_SCHEMA_VERSION,
5
+ createdAt: new Date().toISOString(),
6
+ goal: "",
7
+ userType: "",
8
+ projectPhase: "",
9
+ constraints: [],
10
+ tools: [],
11
+ successCriteria: [],
12
+ nonGoals: [],
13
+ assumptions: [],
14
+ risks: [],
15
+ acceptanceTests: [],
16
+ confidence: { score: 0, explanation: [] }
17
+ };
18
+ }
19
+ export function validateDraft(draft) {
20
+ const errors = [];
21
+ if (!draft.goal)
22
+ errors.push("Goal is required");
23
+ if (!draft.successCriteria || draft.successCriteria.length === 0) {
24
+ errors.push("At least one Success Criterion is required");
25
+ }
26
+ // We can add more strict checks here if needed
27
+ return { valid: errors.length === 0, errors };
28
+ }
@@ -0,0 +1,22 @@
1
+ import inquirer from "inquirer";
2
+ import kleur from "kleur";
3
+ export class InquirerIO {
4
+ async ask(questions) {
5
+ // map Question interface to inquirer question
6
+ // Inquirer types are slightly different, but compatible enough for pass-through usually
7
+ const answers = await inquirer.prompt(questions);
8
+ return answers;
9
+ }
10
+ print(message) {
11
+ console.log(message);
12
+ }
13
+ header(text) {
14
+ console.log(kleur.bold(text));
15
+ }
16
+ divider() {
17
+ console.log(kleur.dim("=========================================="));
18
+ }
19
+ warn(message) {
20
+ console.log(kleur.yellow(message));
21
+ }
22
+ }
@@ -0,0 +1,71 @@
1
+ import fs from "fs";
2
+ import yaml from "js-yaml";
3
+ import kleur from "kleur";
4
+ import { KNOWN_INTENT_FIELDS } from "./types.js";
5
+ export const DEFAULT_INTERVIEW_CONFIG = {
6
+ schemaVersion: 1,
7
+ required: ["successCriteria", "user", "problem"],
8
+ optional: ["nonGoals", "risks", "acceptanceTests"],
9
+ guidance: {
10
+ systemNotes: "You are a Product Manager / Business Analyst. Ask clarifying questions.",
11
+ starterQuestions: [
12
+ "Who is the user and what problem are we solving?",
13
+ "What does success look like (measurable)?",
14
+ "What are the constraints (time, budget, tech, legal)?"
15
+ ],
16
+ redFlags: ["Ambiguous scope", "No success metric"]
17
+ },
18
+ weights: {
19
+ successCriteria: 3,
20
+ risks: 2
21
+ },
22
+ fieldPrompts: {
23
+ successCriteria: "What does success look like? How will we measure it?",
24
+ user: "Who is the specific user for this feature?",
25
+ problem: "What core problem are we solving?",
26
+ risks: "Are there any risks or non-goals we should state?",
27
+ constraints: "Are there any technical or budget constraints?"
28
+ }
29
+ };
30
+ export function loadEffectiveInterviewConfig(framework) {
31
+ if (!framework || !framework.files.interview || !fs.existsSync(framework.files.interview)) {
32
+ return DEFAULT_INTERVIEW_CONFIG;
33
+ }
34
+ try {
35
+ const content = fs.readFileSync(framework.files.interview, "utf8");
36
+ const raw = yaml.load(content);
37
+ // 1. Schema Version Check
38
+ if (raw.schemaVersion !== 1) {
39
+ console.error(kleur.yellow(`IRIS_WARNING: Unsupported interview schema version ${raw.schemaVersion}. Using default.`));
40
+ return DEFAULT_INTERVIEW_CONFIG;
41
+ }
42
+ // 2. Strict Field Validation
43
+ const allFields = [...(raw.required || []), ...(raw.optional || [])];
44
+ const unknownFields = allFields.filter(f => !KNOWN_INTENT_FIELDS.has(f));
45
+ if (unknownFields.length > 0) {
46
+ console.error(kleur.yellow(`IRIS_WARNING: Invalid interview configuration. Unknown fields: ${unknownFields.join(", ")}. Using default.`));
47
+ return DEFAULT_INTERVIEW_CONFIG;
48
+ }
49
+ // 3. Merge with minimal defaults (or just return raw if we trust it completely?)
50
+ // The plan says "If invalid/missing return DEFAULT".
51
+ // But if valid, we return the parsed object.
52
+ // Should we merge fieldPrompts if missing?
53
+ // Best to provide safety defaults for prompts if not overridden.
54
+ return {
55
+ schemaVersion: raw.schemaVersion,
56
+ required: raw.required || [],
57
+ optional: raw.optional || [],
58
+ guidance: {
59
+ systemNotes: raw.guidance?.systemNotes || DEFAULT_INTERVIEW_CONFIG.guidance.systemNotes,
60
+ starterQuestions: raw.guidance?.starterQuestions || DEFAULT_INTERVIEW_CONFIG.guidance.starterQuestions,
61
+ redFlags: raw.guidance?.redFlags || DEFAULT_INTERVIEW_CONFIG.guidance.redFlags
62
+ },
63
+ weights: raw.weights || {},
64
+ fieldPrompts: { ...DEFAULT_INTERVIEW_CONFIG.fieldPrompts, ...raw.fieldPrompts }
65
+ };
66
+ }
67
+ catch (e) {
68
+ console.error(kleur.yellow(`IRIS_WARNING: Failed to parse interview.yaml: ${e.message}. Using default.`));
69
+ return DEFAULT_INTERVIEW_CONFIG;
70
+ }
71
+ }