project-iris 0.0.8 → 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
@@ -0,0 +1,16 @@
1
+ export const KNOWN_INTENT_FIELDS = new Set([
2
+ 'successCriteria',
3
+ 'user',
4
+ 'problem',
5
+ 'constraints',
6
+ 'nonGoals',
7
+ 'risks',
8
+ 'acceptanceTests',
9
+ 'context',
10
+ 'context.user',
11
+ 'context.phase',
12
+ 'visuals',
13
+ 'name', // Sometimes inferred, but valid
14
+ 'description', // Basic
15
+ 'features', // High level
16
+ ]);
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Access a value in an object via dot-notation.
3
+ * Safe traversal: returns undefined if path breaks.
4
+ */
5
+ export function getByPath(obj, path) {
6
+ if (!obj)
7
+ return undefined;
8
+ const parts = path.split('.');
9
+ let current = obj;
10
+ for (const part of parts) {
11
+ if (current === null || current === undefined)
12
+ return undefined;
13
+ current = current[part];
14
+ }
15
+ return current;
16
+ }
17
+ /**
18
+ * Checks if a value is "missing" according to strict interview rules.
19
+ * Missing = null, undefined, empty string "", whitespace-only string, or empty array [].
20
+ */
21
+ export function isMissing(value) {
22
+ if (value === null || value === undefined)
23
+ return true;
24
+ if (typeof value === 'string')
25
+ return value.trim().length === 0;
26
+ if (Array.isArray(value))
27
+ return value.length === 0;
28
+ // Objects? We typically check leaf nodes, but an empty object {} usually counts as present for 'context'
29
+ // UNLESS the requirement is specifically context.user.
30
+ // So for generic objects, existence is enough.
31
+ return false;
32
+ }
33
+ /**
34
+ * Returns a list of required fields that are missing in the draft.
35
+ */
36
+ export function getMissingFields(draft, requiredFields) {
37
+ return requiredFields.filter(field => isMissing(getByPath(draft, field)));
38
+ }
@@ -3,11 +3,11 @@ import path from "path";
3
3
  import kleur from "kleur";
4
4
  import { repoRoot, writeFile, ensureDir } from "../lib.js";
5
5
  import { loadState } from "./state.js";
6
- import { loadPolicy } from "./policy.js";
6
+ import { loadEffectivePolicy } from "./policy.js";
7
7
  import { validate } from "./validator.js";
8
8
  import { resolveIncludes, getFileContent } from "./include.js";
9
9
  import { checkArtifact } from "./artifact-checker.js";
10
- import { EXIT_CODES } from "../utils/exit-codes.js";
10
+ import { collectDoctrineFiles } from "./doctrine/collector.js";
11
11
  export async function generatePack(options) {
12
12
  const root = repoRoot();
13
13
  // 1. Validate Repo (Guardrail)
@@ -15,20 +15,27 @@ export async function generatePack(options) {
15
15
  const result = await validate({
16
16
  apply: false,
17
17
  strict: false,
18
- writeBack: false
18
+ writeBack: false,
19
+ frameworkResolution: options.frameworkResolution
19
20
  });
20
21
  if (!result.valid) {
21
- console.error(kleur.red("Repo is INVALID. Fix issues before packing."));
22
- process.exit(EXIT_CODES.INVALID); // Exit 2
22
+ const msg = !options.quiet ? kleur.red("Repo is INVALID. Fix issues before packing.") : "Repo is INVALID";
23
+ if (!options.quiet)
24
+ console.error(msg);
25
+ throw new Error(typeof msg === 'string' ? msg : "Repo is INVALID");
23
26
  }
24
27
  if (options.strict && result.warnings) {
25
- console.error(kleur.red("Repo has warnings and strict mode is on."));
26
- process.exit(EXIT_CODES.INVALID);
28
+ const msg = !options.quiet ? kleur.red("Repo has warnings and strict mode is on.") : "Repo has warnings";
29
+ if (!options.quiet)
30
+ console.error(msg);
31
+ throw new Error(typeof msg === 'string' ? msg : "Repo has warnings");
27
32
  }
28
33
  }
29
34
  catch (e) {
30
- console.error("Validation failed:", e);
31
- process.exit(EXIT_CODES.INVALID);
35
+ // If policy loading fails, we should catch it here
36
+ if (!options.quiet)
37
+ console.error("Validation failed:", e);
38
+ throw new Error(`Validation failed: ${e.message}`);
32
39
  }
33
40
  // 2. Load State & Determine Context
34
41
  const state = loadState();
@@ -36,9 +43,9 @@ export async function generatePack(options) {
36
43
  const agent = options.agent || inferAgent(phase);
37
44
  // 3. Collect Candidates
38
45
  // Doctrine
39
- const doctrineFiles = collectDoctrine(agent);
46
+ const doctrineFiles = collectDoctrine(root, state.active.flow);
40
47
  // Required Artifacts
41
- const requiredArtifacts = collectRequiredArtifacts(phase, state);
48
+ const requiredArtifacts = collectRequiredArtifacts(phase, state, options.frameworkResolution);
42
49
  // Optional Artifacts
43
50
  const optionalArtifacts = collectOptionalArtifacts(phase);
44
51
  // Included Artifacts
@@ -58,13 +65,18 @@ export async function generatePack(options) {
58
65
  // Use artifact checker for proper validation
59
66
  const check = checkArtifact(root, p);
60
67
  if (!check.exists) {
61
- console.error(kleur.red(`Missing required artifact: ${check.normalized}`));
62
- process.exit(EXIT_CODES.INVALID);
68
+ const msg = `Missing required artifact: ${check.normalized}`;
69
+ if (!options.quiet)
70
+ console.error(kleur.red(msg));
71
+ throw new Error(msg);
63
72
  }
64
73
  // For files, also check if content was readable
74
+ // Absolute path fix in resolver.ts implies this works now
65
75
  if (check.isFile && res.skipped === "read_error") {
66
- console.error(kleur.red(`Cannot read required artifact: ${check.normalized}`));
67
- process.exit(EXIT_CODES.INVALID);
76
+ const msg = `Cannot read required artifact: ${check.normalized}`;
77
+ if (!options.quiet)
78
+ console.error(kleur.red(msg));
79
+ throw new Error(msg);
68
80
  }
69
81
  }
70
82
  coreContent.push(res);
@@ -86,7 +98,7 @@ export async function generatePack(options) {
86
98
  }
87
99
  // 5. Enforce Limits
88
100
  const maxBytes = options.maxBytes || 1_500_000;
89
- const finalSelection = enforceLimits(coreContent, optionalContent, extraContent, maxBytes);
101
+ const finalSelection = enforceLimits(coreContent, optionalContent, extraContent, maxBytes, options.quiet);
90
102
  // 6. Format
91
103
  const markdown = formatBundle(finalSelection, {
92
104
  repo: path.basename(root),
@@ -95,7 +107,7 @@ export async function generatePack(options) {
95
107
  generatedAt: new Date().toISOString()
96
108
  });
97
109
  // 7. Output
98
- if (options.stdout) {
110
+ if (options.stdout && !options.json) {
99
111
  console.log(markdown);
100
112
  if (options.output) {
101
113
  writeFile(options.output, markdown);
@@ -103,15 +115,36 @@ export async function generatePack(options) {
103
115
  }
104
116
  else {
105
117
  const outFile = options.output || path.join(root, `.iris/inbox/context-${phase}-${agent}.md`);
106
- writeFile(outFile, markdown);
107
- // Write LATEST
108
- const latestFile = path.join(root, ".iris/inbox/LATEST.md");
109
- ensureDir(path.dirname(latestFile)); // Ensure dir exists for LATEST too
110
- const relPath = path.relative(path.dirname(latestFile), outFile);
111
- writeFile(latestFile, relPath);
112
- console.log(kleur.green(`Packed context bundle to: ${outFile}`));
113
- console.log(kleur.gray(`Size: ${(markdown.length / 1024).toFixed(1)} KB`));
118
+ let fileWritten = false;
119
+ if (!options.stdout) {
120
+ writeFile(outFile, markdown);
121
+ // Write LATEST
122
+ const latestFile = path.join(root, ".iris/inbox/LATEST.md");
123
+ ensureDir(path.dirname(latestFile));
124
+ const relPath = path.relative(path.dirname(latestFile), outFile);
125
+ writeFile(latestFile, relPath);
126
+ fileWritten = true;
127
+ }
128
+ const allCount = finalSelection.core.length + finalSelection.optional.length + finalSelection.extra.length;
129
+ const result = {
130
+ path: fileWritten ? outFile : "stdout",
131
+ size: markdown.length,
132
+ fileCount: allCount
133
+ };
134
+ if (options.json && !options.quiet) {
135
+ console.log(JSON.stringify(result, null, 2));
136
+ }
137
+ else if (!options.stdout && !options.quiet) {
138
+ console.log(kleur.green(`Packed context bundle to: ${outFile}`));
139
+ console.log(kleur.gray(`Size: ${(markdown.length / 1024).toFixed(1)} KB`));
140
+ }
141
+ return result;
114
142
  }
143
+ return {
144
+ path: options.output || "stdout",
145
+ size: markdown.length,
146
+ fileCount: finalSelection.core.length + finalSelection.optional.length + finalSelection.extra.length
147
+ };
115
148
  }
116
149
  function inferAgent(phase) {
117
150
  switch (phase) {
@@ -121,21 +154,11 @@ function inferAgent(phase) {
121
154
  default: return "master";
122
155
  }
123
156
  }
124
- function collectDoctrine(agent) {
125
- const list = [
126
- ".iris/aidlc/quick-start.md",
127
- ".iris/aidlc/context/load-order.md",
128
- ".iris/aidlc/standards/doctrine-structure.md",
129
- ".iris/aidlc/standards/phases-and-gates.md",
130
- ".iris/aidlc/validation/phase-preconditions.md",
131
- ".iris/aidlc/standards/artifacts-registry.md",
132
- ".iris/aidlc/standards/tool-wrappers.md",
133
- `.iris/aidlc/agents/iris-${agent}-agent.md`,
134
- `.iris/aidlc/commands/iris-${agent}-agent.md`
135
- ];
136
- return list;
157
+ function collectDoctrine(root, activeFlowId) {
158
+ // Replaced legacy hardcoded list with flow-aware collector
159
+ return collectDoctrineFiles(root, activeFlowId);
137
160
  }
138
- function collectRequiredArtifacts(phase, state) {
161
+ function collectRequiredArtifacts(phase, state, framework) {
139
162
  const list = [
140
163
  ".iris/state.yaml",
141
164
  ".iris/policy.yaml"
@@ -146,7 +169,7 @@ function collectRequiredArtifacts(phase, state) {
146
169
  list.push(".iris/manifest.yaml");
147
170
  // Use Policy Source of Truth
148
171
  try {
149
- const policy = loadPolicy();
172
+ const policy = loadEffectivePolicy(framework, undefined, state.active.flow);
150
173
  if (policy.phases[phase] && policy.phases[phase].requires) {
151
174
  for (const req of policy.phases[phase].requires) {
152
175
  list.push(req.path);
@@ -192,7 +215,7 @@ function getLatestFiles(dir, count) {
192
215
  });
193
216
  return files.slice(0, count).map(f => f.path);
194
217
  }
195
- function enforceLimits(core, optional, extra, maxBytes) {
218
+ function enforceLimits(core, optional, extra, maxBytes, quiet) {
196
219
  let currentSize = sumSize(core) + sumSize(optional) + sumSize(extra);
197
220
  const errors = [];
198
221
  const skipped = [];
@@ -216,9 +239,11 @@ function enforceLimits(core, optional, extra, maxBytes) {
216
239
  }
217
240
  if (currentSize > maxBytes) {
218
241
  // Hard fail if Core is too big?
219
- console.error(kleur.red(`Bundle size (${currentSize}) exceeds --max-bytes limit (${maxBytes}) even after trimming optional content.`));
220
- console.error("Required content is too large. Increase limit or clean up artifacts.");
221
- process.exit(EXIT_CODES.INVALID);
242
+ if (!quiet)
243
+ console.error(kleur.red(`Bundle size (${currentSize}) exceeds --max-bytes limit (${maxBytes}) even after trimming optional content.`));
244
+ if (!quiet)
245
+ console.error("Required content is too large. Increase limit or clean up artifacts.");
246
+ throw new Error(`Bundle size exceeds limit ${maxBytes}`);
222
247
  }
223
248
  return {
224
249
  core,
@@ -244,9 +269,6 @@ function formatBundle(selection, meta) {
244
269
  out += `- This bundle contains doctrine + required artifacts.\n\n`;
245
270
  // Groups
246
271
  const all = [...core, ...optional, ...extra];
247
- // Separate Doctrine from others?
248
- // Core has doctrine + state + required.
249
- // Let's group by type roughly using path analysis
250
272
  const doctrine = all.filter(f => f.path.startsWith(".iris/aidlc"));
251
273
  const stateFiles = all.filter(f => f.path.startsWith(".iris/") && !f.path.startsWith(".iris/aidlc"));
252
274
  const memory = all.filter(f => f.path.startsWith("memory-bank/"));
@@ -0,0 +1,43 @@
1
+ export function parseUnitList(content) {
2
+ const lines = content.split('\n');
3
+ const units = [];
4
+ let currentUnit = null;
5
+ for (const line of lines) {
6
+ // 1. Check for Unit Header: - [ ] U001: Title
7
+ const headerMatch = line.match(/^- \[([ x])\] (U\d+): (.+)$/);
8
+ if (headerMatch) {
9
+ if (currentUnit) {
10
+ units.push(currentUnit);
11
+ }
12
+ const isChecked = headerMatch[1] === 'x';
13
+ currentUnit = {
14
+ id: headerMatch[2],
15
+ title: headerMatch[3].trim(),
16
+ summary: '',
17
+ status: isChecked ? 'done' : 'todo'
18
+ };
19
+ continue;
20
+ }
21
+ // 2. Check for Summary: summary: ... (must be indented)
22
+ // We only accept lines starting with whitespace + "summary:"
23
+ if (currentUnit) {
24
+ const summaryMatch = line.match(/^\s+summary:\s*(.+)$/);
25
+ if (summaryMatch) {
26
+ currentUnit.summary = summaryMatch[1].trim();
27
+ }
28
+ }
29
+ // Ignore other lines
30
+ }
31
+ if (currentUnit) {
32
+ units.push(currentUnit);
33
+ }
34
+ // Validate unique IDs
35
+ const ids = new Set();
36
+ for (const u of units) {
37
+ if (ids.has(u.id)) {
38
+ throw new Error(`Duplicate Unit ID found: ${u.id}`);
39
+ }
40
+ ids.add(u.id);
41
+ }
42
+ return units;
43
+ }
@@ -0,0 +1,18 @@
1
+ import path from "path";
2
+ export const IRIS_DOT_DIR = ".iris";
3
+ export const IRIS_INBOX_DIR = path.join(IRIS_DOT_DIR, "inbox");
4
+ export const INTENT_DRAFT_LATEST_FILE = "intent-draft.latest.json";
5
+ export const INTENT_DRAFT_LEGACY_FILE = "intent-draft.json";
6
+ export function getInboxPath(root) {
7
+ return path.join(root, IRIS_INBOX_DIR);
8
+ }
9
+ export function getLatestDraftPath(root) {
10
+ return path.join(getInboxPath(root), INTENT_DRAFT_LATEST_FILE);
11
+ }
12
+ export function getLegacyDraftPath(root) {
13
+ return path.join(getInboxPath(root), INTENT_DRAFT_LEGACY_FILE);
14
+ }
15
+ export function getHistoryDraftPath(root, timestamp) {
16
+ const ts = timestamp.toISOString().replace(/[-:T]/g, "").split(".")[0];
17
+ return path.join(getInboxPath(root), `intent-draft-${ts}.json`);
18
+ }
@@ -1,28 +1,133 @@
1
1
  import fs from "fs";
2
+ import path from "path";
2
3
  import yaml from "js-yaml";
3
- import { EXIT_CODES } from "../utils/exit-codes.js";
4
+ import kleur from "kleur";
5
+ export class PolicyError extends Error {
6
+ constructor(message) {
7
+ super(message);
8
+ this.name = "PolicyError";
9
+ }
10
+ }
11
+ export class PolicyLoadError extends PolicyError {
12
+ cause;
13
+ constructor(message, cause) {
14
+ super(message);
15
+ this.cause = cause;
16
+ this.name = "PolicyLoadError";
17
+ }
18
+ }
4
19
  import { resolveArtifactPath } from "./resolver.js";
5
20
  import { repoRoot } from "../lib.js";
6
- export function loadPolicy() {
21
+ // --- Loaders ---
22
+ /**
23
+ * Loads the base policy from the active framework OR fallback to legacy .iris/policy.yaml.
24
+ */
25
+ export function loadBasePolicy(framework, root) {
7
26
  try {
8
- const root = repoRoot();
9
- const policyPath = resolveArtifactPath(root, ".iris/policy.yaml");
10
- if (!fs.existsSync(policyPath)) {
11
- // Policy is REQUIRED.
12
- console.error(`Missing required policy file: ${policyPath}`);
13
- process.exit(EXIT_CODES.POLICY_ERROR);
27
+ const r = root || repoRoot();
28
+ // 1. Framework Priority
29
+ if (framework && framework.files.policy && fs.existsSync(framework.files.policy)) {
30
+ const content = fs.readFileSync(framework.files.policy, "utf8");
31
+ return parsePolicy(content);
32
+ }
33
+ // 2. Legacy Fallback
34
+ const legacyPath = resolveArtifactPath(r, ".iris/policy.yaml");
35
+ if (fs.existsSync(legacyPath)) {
36
+ if (framework) {
37
+ console.error(kleur.yellow(`IRIS_WARNING IRIS_DEPRECATED_LEGACY_POLICY: framework=${framework.manifest.id} missing policy.yaml; using .iris/policy.yaml`));
38
+ }
39
+ const content = fs.readFileSync(legacyPath, "utf8");
40
+ return parsePolicy(content);
14
41
  }
15
- const content = fs.readFileSync(policyPath, "utf8");
16
- const doc = yaml.load(content);
17
- // Basic structural check
18
- if (!doc || !doc.phases || !doc.transitions) {
19
- console.error("Invalid policy file format: 'phases' and 'transitions' are required.");
20
- process.exit(EXIT_CODES.POLICY_ERROR);
42
+ // 3. Hard Failure
43
+ throw new PolicyLoadError(`Missing required policy file. No active framework policy and no .iris/policy.yaml found at ${legacyPath}`);
44
+ }
45
+ catch (error) {
46
+ if (error instanceof PolicyError)
47
+ throw error;
48
+ throw new PolicyLoadError("Failed to load base policy", error);
49
+ }
50
+ }
51
+ function parsePolicy(content) {
52
+ const doc = yaml.load(content);
53
+ // Basic structural check
54
+ if (!doc || !doc.phases || !doc.transitions) {
55
+ throw new PolicyLoadError("Invalid base policy format: 'phases' and 'transitions' are required.");
56
+ }
57
+ return doc;
58
+ }
59
+ /**
60
+ * Legacy alias for backward compatibility or simple usage.
61
+ */
62
+ export const loadPolicy = (root) => loadBasePolicy(null, root);
63
+ /**
64
+ * Loads the Effective Policy: Base + User Repo Overlay.
65
+ */
66
+ export function loadEffectivePolicy(framework, root, activeFlowId) {
67
+ const r = root || repoRoot();
68
+ const base = loadBasePolicy(framework, r);
69
+ // Determine Overlay Namespace: Flow ID (if active) OR Framework ID (if loaded)
70
+ const overlayNamespace = activeFlowId || framework?.manifest.id;
71
+ if (!overlayNamespace) {
72
+ return base;
73
+ }
74
+ try {
75
+ // User Repo Overlay: .iris/overlays/<namespace>/policy.yaml
76
+ const overlayPath = path.join(r, ".iris/overlays", overlayNamespace, "policy.yaml");
77
+ if (fs.existsSync(overlayPath)) {
78
+ const content = fs.readFileSync(overlayPath, "utf8");
79
+ const overlay = (yaml.load(content) || {});
80
+ return mergePolicies(base, overlay);
21
81
  }
22
- return doc;
82
+ // Return base if no overlay
83
+ return base;
23
84
  }
24
85
  catch (error) {
25
- console.error("Failed to load .iris/policy.yaml:", error);
26
- process.exit(EXIT_CODES.POLICY_ERROR);
86
+ if (error instanceof PolicyError)
87
+ throw error;
88
+ throw new PolicyLoadError(`Failed to load effective policy for namespace '${overlayNamespace}'`, error);
89
+ }
90
+ }
91
+ /**
92
+ * Merges a base policy with an overlay policy purely.
93
+ */
94
+ export function mergePolicies(base, overlay) {
95
+ // Deep clone base using JSON serialization
96
+ const result = JSON.parse(JSON.stringify(base));
97
+ if (!overlay.phases)
98
+ return result;
99
+ for (const [phaseName, overlayPhase] of Object.entries(overlay.phases)) {
100
+ if (!overlayPhase)
101
+ continue;
102
+ // If base doesn't have this phase, initialize it
103
+ if (!result.phases[phaseName]) {
104
+ result.phases[phaseName] = { requires: [], gates: [] };
105
+ }
106
+ const basePhase = result.phases[phaseName];
107
+ // 1. Merge Requirements (Union by path)
108
+ if (overlayPhase.requires && Array.isArray(overlayPhase.requires)) {
109
+ const baseMap = new Map();
110
+ (basePhase.requires || []).forEach((r, idx) => baseMap.set(r.path, idx));
111
+ if (!basePhase.requires)
112
+ basePhase.requires = [];
113
+ for (const req of overlayPhase.requires) {
114
+ if (baseMap.has(req.path)) {
115
+ // OVERRIDE in place
116
+ const idx = baseMap.get(req.path);
117
+ basePhase.requires[idx] = { ...basePhase.requires[idx], ...req };
118
+ }
119
+ else {
120
+ // APPEND
121
+ basePhase.requires.push(req);
122
+ }
123
+ }
124
+ }
125
+ // 2. Merge Gates (Concatenate)
126
+ if (overlayPhase.gates && Array.isArray(overlayPhase.gates)) {
127
+ if (!basePhase.gates)
128
+ basePhase.gates = [];
129
+ basePhase.gates.push(...overlayPhase.gates);
130
+ }
27
131
  }
132
+ return result;
28
133
  }
@@ -0,0 +1,56 @@
1
+ import { spawn } from 'child_process';
2
+ export class ExecError extends Error {
3
+ command;
4
+ args;
5
+ result;
6
+ constructor(command, args, result) {
7
+ super(`Command failed with code ${result.code}: ${command} ${args.join(' ')}\nStderr: ${result.stderr}`);
8
+ this.command = command;
9
+ this.args = args;
10
+ this.result = result;
11
+ this.name = 'ExecError';
12
+ }
13
+ }
14
+ export async function runCmd(cmd, args, opts = { cwd: process.cwd() }) {
15
+ return new Promise((resolve, reject) => {
16
+ if (opts.verbose) {
17
+ console.log(`> ${cmd} ${args.join(' ')} (in ${opts.cwd})`);
18
+ }
19
+ const child = spawn(cmd, args, {
20
+ cwd: opts.cwd,
21
+ env: { ...process.env, ...opts.env },
22
+ shell: false, // Safer to not use shell
23
+ stdio: 'pipe',
24
+ });
25
+ let stdout = '';
26
+ let stderr = '';
27
+ child.stdout.on('data', (data) => {
28
+ const chunk = data.toString();
29
+ stdout += chunk;
30
+ if (opts.verbose)
31
+ process.stdout.write(chunk);
32
+ });
33
+ child.stderr.on('data', (data) => {
34
+ const chunk = data.toString();
35
+ stderr += chunk;
36
+ if (opts.verbose)
37
+ process.stderr.write(chunk);
38
+ });
39
+ child.on('error', (err) => {
40
+ reject(err);
41
+ });
42
+ child.on('close', (code) => {
43
+ const result = {
44
+ code: code ?? 1, // Treat signal termination as error code 1
45
+ stdout,
46
+ stderr,
47
+ };
48
+ if (code === 0) {
49
+ resolve(result);
50
+ }
51
+ else {
52
+ reject(new ExecError(cmd, args, result));
53
+ }
54
+ });
55
+ });
56
+ }
@@ -38,6 +38,9 @@ export function resolveIrisRoot(repoRoot) {
38
38
  * to its absolute physical location on disk, respecting the IRIS source authority.
39
39
  */
40
40
  export function resolveArtifactPath(repoRoot, relativePath) {
41
+ if (path.isAbsolute(relativePath)) {
42
+ return relativePath;
43
+ }
41
44
  if (relativePath.startsWith(".iris/") || relativePath === ".iris") {
42
45
  const root = resolveIrisRoot(repoRoot);
43
46
  if (relativePath === ".iris")