project-iris 0.0.12 → 0.0.14
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.
- package/README.md +214 -323
- package/bin/cli.js +21 -0
- package/flows/aidlc/README.md +372 -0
- package/flows/aidlc/agents/construction-agent.md +79 -0
- package/flows/aidlc/agents/inception-agent.md +97 -0
- package/flows/aidlc/agents/master-agent.md +61 -0
- package/flows/aidlc/agents/operations-agent.md +89 -0
- package/flows/aidlc/commands/construction-agent.md +63 -0
- package/flows/aidlc/commands/inception-agent.md +55 -0
- package/flows/aidlc/commands/master-agent.md +47 -0
- package/flows/aidlc/commands/operations-agent.md +77 -0
- package/flows/aidlc/context-config.yaml +67 -0
- package/flows/aidlc/memory-bank.yaml +104 -0
- package/flows/aidlc/quick-start.md +322 -0
- package/flows/aidlc/skills/construction/bolt-list.md +163 -0
- package/flows/aidlc/skills/construction/bolt-replan.md +345 -0
- package/flows/aidlc/skills/construction/bolt-start.md +442 -0
- package/flows/aidlc/skills/construction/bolt-status.md +185 -0
- package/flows/aidlc/skills/construction/navigator.md +196 -0
- package/flows/aidlc/skills/inception/bolt-plan.md +372 -0
- package/flows/aidlc/skills/inception/context.md +171 -0
- package/flows/aidlc/skills/inception/intent-create.md +211 -0
- package/flows/aidlc/skills/inception/intent-list.md +124 -0
- package/flows/aidlc/skills/inception/navigator.md +207 -0
- package/flows/aidlc/skills/inception/requirements.md +227 -0
- package/flows/aidlc/skills/inception/review.md +248 -0
- package/flows/aidlc/skills/inception/story-create.md +304 -0
- package/flows/aidlc/skills/inception/units.md +278 -0
- package/flows/aidlc/skills/master/analyze-context.md +239 -0
- package/flows/aidlc/skills/master/answer-question.md +141 -0
- package/flows/aidlc/skills/master/explain-flow.md +158 -0
- package/flows/aidlc/skills/master/project-init.md +281 -0
- package/flows/aidlc/skills/master/route-request.md +126 -0
- package/flows/aidlc/skills/operations/build.md +237 -0
- package/flows/aidlc/skills/operations/deploy.md +259 -0
- package/flows/aidlc/skills/operations/monitor.md +265 -0
- package/flows/aidlc/skills/operations/navigator.md +209 -0
- package/flows/aidlc/skills/operations/verify.md +224 -0
- package/{dist/iris_bundle/frameworks/iris-core → flows/aidlc}/templates/construction/bolt-types/ddd-construction-bolt.md +3 -3
- package/{dist → flows/aidlc}/templates/construction/bolt-types/spike-bolt.md +2 -2
- package/flows/aidlc/templates/construction/construction-log-template.md +129 -0
- package/flows/aidlc/templates/construction/standards/coding-standards.md +29 -0
- package/flows/aidlc/templates/construction/standards/system-architecture.md +22 -0
- package/flows/aidlc/templates/construction/standards/tech-stack.md +19 -0
- package/flows/aidlc/templates/inception/inception-log-template.md +134 -0
- package/flows/aidlc/templates/inception/project/README.md +55 -0
- package/flows/aidlc/templates/standards/catalog.yaml +345 -0
- package/flows/aidlc/templates/standards/coding-standards.guide.md +553 -0
- package/flows/aidlc/templates/standards/data-stack.guide.md +162 -0
- package/flows/aidlc/templates/standards/tech-stack.guide.md +280 -0
- package/lib/InstallerFactory.js +36 -0
- package/lib/analytics/env-detector.js +92 -0
- package/lib/analytics/index.js +22 -0
- package/lib/analytics/machine-id.js +33 -0
- package/lib/analytics/tracker.js +232 -0
- package/lib/cli-utils.js +342 -0
- package/lib/constants.js +32 -0
- package/lib/installer.js +402 -0
- package/lib/installers/AntigravityInstaller.js +22 -0
- package/lib/installers/ClaudeInstaller.js +85 -0
- package/lib/installers/ClineInstaller.js +21 -0
- package/lib/installers/CodexInstaller.js +21 -0
- package/lib/installers/CopilotInstaller.js +113 -0
- package/lib/installers/CursorInstaller.js +63 -0
- package/lib/installers/GeminiInstaller.js +75 -0
- package/lib/installers/KiroInstaller.js +22 -0
- package/lib/installers/OpenCodeInstaller.js +22 -0
- package/lib/installers/RooInstaller.js +22 -0
- package/lib/installers/ToolInstaller.js +73 -0
- package/lib/installers/WindsurfInstaller.js +22 -0
- package/lib/markdown-validator.ts +175 -0
- package/lib/yaml-validator.ts +99 -0
- package/package.json +105 -32
- package/scripts/artifact-validator.js +594 -0
- package/scripts/bolt-complete.js +606 -0
- package/scripts/status-integrity.js +598 -0
- package/dist/bridge/agent-runner.js +0 -190
- package/dist/bridge/connector-factory.js +0 -31
- package/dist/bridge/connectors/antigravity-connector.js +0 -18
- package/dist/bridge/connectors/cursor-connector.js +0 -31
- package/dist/bridge/connectors/in-process-connector.js +0 -29
- package/dist/bridge/connectors/vscode-connector.js +0 -31
- package/dist/bridge/connectors/windsurf-connector.js +0 -23
- package/dist/bridge/filesystem-connector.js +0 -110
- package/dist/bridge/helper.js +0 -203
- package/dist/bridge/types.js +0 -10
- package/dist/cli.js +0 -40
- package/dist/commands/ask.js +0 -259
- package/dist/commands/bridge.js +0 -88
- package/dist/commands/create.js +0 -25
- package/dist/commands/develop.js +0 -141
- package/dist/commands/doctor.js +0 -102
- package/dist/commands/flow.js +0 -301
- package/dist/commands/framework.js +0 -273
- package/dist/commands/generate.js +0 -59
- package/dist/commands/install.js +0 -100
- package/dist/commands/pack.js +0 -33
- package/dist/commands/phase.js +0 -38
- package/dist/commands/run.js +0 -199
- package/dist/commands/status.js +0 -114
- package/dist/commands/uninstall.js +0 -14
- package/dist/commands/use.js +0 -20
- package/dist/commands/validate.js +0 -102
- package/dist/framework/framework-loader.js +0 -97
- package/dist/framework/framework-paths.js +0 -48
- package/dist/framework/framework-types.js +0 -15
- package/dist/iris/artifact-checker.js +0 -78
- package/dist/iris/artifacts/config.js +0 -68
- package/dist/iris/artifacts/generator.js +0 -88
- package/dist/iris/artifacts/types.js +0 -1
- package/dist/iris/bundle.js +0 -44
- package/dist/iris/doctrine/collector.js +0 -124
- package/dist/iris/fixer.js +0 -149
- package/dist/iris/flows/manifest.js +0 -124
- package/dist/iris/framework-context.js +0 -49
- package/dist/iris/framework-manager.js +0 -215
- package/dist/iris/fs/atomic.js +0 -22
- package/dist/iris/guard.js +0 -38
- package/dist/iris/importers/index.js +0 -9
- package/dist/iris/importers/types.js +0 -8
- package/dist/iris/importers/writer.js +0 -139
- package/dist/iris/include.js +0 -49
- package/dist/iris/installer.js +0 -334
- package/dist/iris/interactive/env.js +0 -21
- package/dist/iris/interactive/intent-interview.js +0 -345
- package/dist/iris/interactive/intent-schema.js +0 -28
- package/dist/iris/interactive/interview-io.js +0 -22
- package/dist/iris/interview/config.js +0 -71
- package/dist/iris/interview/types.js +0 -16
- package/dist/iris/interview/utils.js +0 -38
- package/dist/iris/manifest.js +0 -54
- package/dist/iris/packer.js +0 -325
- package/dist/iris/parsers/unit-parser.js +0 -43
- package/dist/iris/paths.js +0 -18
- package/dist/iris/policy.js +0 -133
- package/dist/iris/proc.js +0 -56
- package/dist/iris/report.js +0 -53
- package/dist/iris/resolver.js +0 -66
- package/dist/iris/router.js +0 -114
- package/dist/iris/routes.js +0 -189
- package/dist/iris/run-state.js +0 -146
- package/dist/iris/state.js +0 -113
- package/dist/iris/templates.js +0 -70
- package/dist/iris/tmp.js +0 -24
- package/dist/iris/uninstaller.js +0 -181
- package/dist/iris/utils/interpolate.js +0 -42
- package/dist/iris/validator.js +0 -391
- package/dist/iris/workflow/config.js +0 -51
- package/dist/iris/workflow/engine.js +0 -129
- package/dist/iris/workflow/steps.js +0 -448
- package/dist/iris/workflow/types.js +0 -1
- package/dist/iris_bundle/frameworks/iris-core/framework.yaml +0 -9
- package/dist/iris_bundle/frameworks/iris-core/memory/memory-bank.yaml +0 -1
- package/dist/iris_bundle/frameworks/iris-core/policy.yaml +0 -7
- package/dist/iris_bundle/frameworks/iris-core/templates/config/memory-bank.yaml +0 -1
- package/dist/iris_bundle/frameworks/iris-core/templates/construction/bolt-types/spike-bolt.md +0 -240
- package/dist/lib.js +0 -96
- package/dist/templates/construction/bolt-template.md +0 -226
- package/dist/templates/construction/bolt-types/ddd-construction-bolt/adr-template.md +0 -49
- package/dist/templates/construction/bolt-types/ddd-construction-bolt/ddd-01-domain-model-template.md +0 -55
- package/dist/templates/construction/bolt-types/ddd-construction-bolt/ddd-02-technical-design-template.md +0 -67
- package/dist/templates/construction/bolt-types/ddd-construction-bolt/ddd-03-test-report-template.md +0 -62
- package/dist/templates/construction/bolt-types/ddd-construction-bolt.md +0 -528
- package/dist/templates/construction/bolt-types/simple-construction-bolt.md +0 -347
- package/dist/templates/inception/requirements-template.md +0 -144
- package/dist/templates/inception/stories-template.md +0 -38
- package/dist/templates/inception/story-template.md +0 -147
- package/dist/templates/inception/system-context-template.md +0 -29
- package/dist/templates/inception/unit-brief-template.md +0 -177
- package/dist/templates/inception/units-template.md +0 -52
- package/dist/utils/exit-codes.js +0 -7
- package/dist/utils/logo.js +0 -17
- package/dist/workflows/bolt-execution.js +0 -238
- package/dist/workflows/bolt-plan.js +0 -221
- package/dist/workflows/intent-inception.js +0 -285
- package/dist/workflows/memory-bank-generator.js +0 -180
- package/dist/workflows/reporting.js +0 -74
- /package/{dist/iris_bundle/frameworks/iris-core → flows/aidlc}/templates/construction/bolt-template.md +0 -0
- /package/{dist/iris_bundle/frameworks/iris-core → flows/aidlc}/templates/construction/bolt-types/ddd-construction-bolt/adr-template.md +0 -0
- /package/{dist/iris_bundle/frameworks/iris-core → flows/aidlc}/templates/construction/bolt-types/ddd-construction-bolt/ddd-01-domain-model-template.md +0 -0
- /package/{dist/iris_bundle/frameworks/iris-core → flows/aidlc}/templates/construction/bolt-types/ddd-construction-bolt/ddd-02-technical-design-template.md +0 -0
- /package/{dist/iris_bundle/frameworks/iris-core → flows/aidlc}/templates/construction/bolt-types/ddd-construction-bolt/ddd-03-test-report-template.md +0 -0
- /package/{dist/iris_bundle/frameworks/iris-core → flows/aidlc}/templates/construction/bolt-types/simple-construction-bolt.md +0 -0
- /package/{dist/iris_bundle/frameworks/iris-core → flows/aidlc}/templates/inception/requirements-template.md +0 -0
- /package/{dist/iris_bundle/frameworks/iris-core → flows/aidlc}/templates/inception/stories-template.md +0 -0
- /package/{dist/iris_bundle/frameworks/iris-core → flows/aidlc}/templates/inception/story-template.md +0 -0
- /package/{dist/iris_bundle/frameworks/iris-core → flows/aidlc}/templates/inception/system-context-template.md +0 -0
- /package/{dist/iris_bundle/frameworks/iris-core → flows/aidlc}/templates/inception/unit-brief-template.md +0 -0
- /package/{dist/iris_bundle/frameworks/iris-core → flows/aidlc}/templates/inception/units-template.md +0 -0
package/dist/iris/resolver.js
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import { fileURLToPath } from "url";
|
|
4
|
-
// Get __dirname equivalent in ESM
|
|
5
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
-
const __dirname = path.dirname(__filename);
|
|
7
|
-
/**
|
|
8
|
-
* Resolves the root directory of the active IRIS configuration (.iris folder).
|
|
9
|
-
* Strategy:
|
|
10
|
-
* 1. Check <repoRoot>/.iris
|
|
11
|
-
* 2. Fallback to bundled <package>/.iris (e.g. ../iris_bundle/.iris)
|
|
12
|
-
*/
|
|
13
|
-
export function resolveIrisRoot(repoRoot) {
|
|
14
|
-
const repoIris = path.join(repoRoot, ".iris");
|
|
15
|
-
// Check for policy.yaml to confirm this is a valid IRIS configuration authority
|
|
16
|
-
if (fs.existsSync(path.join(repoIris, "policy.yaml"))) {
|
|
17
|
-
return { path: repoIris, source: "repo" };
|
|
18
|
-
}
|
|
19
|
-
// Fallback to bundled
|
|
20
|
-
// We assume this file is in .../iris/resolver.js|ts
|
|
21
|
-
// And bundle is in .../iris_bundle/.iris
|
|
22
|
-
let bundledIris = path.resolve(__dirname, "../iris_bundle/.iris");
|
|
23
|
-
// Safety check (bundled should always exist, but in dev vs prod checks...)
|
|
24
|
-
if (!fs.existsSync(bundledIris)) {
|
|
25
|
-
// Try finding it in src if we are in dist (dev environment fallback)
|
|
26
|
-
const srcBundle = path.resolve(__dirname, "../../src/iris_bundle/.iris");
|
|
27
|
-
if (fs.existsSync(srcBundle)) {
|
|
28
|
-
bundledIris = srcBundle;
|
|
29
|
-
}
|
|
30
|
-
else {
|
|
31
|
-
console.warn(`[IRIS] Warning: Bundled IRIS not found at ${bundledIris}`);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
return { path: bundledIris, source: "bundled" };
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Resolves a relative path (like ".iris/policy.yaml" or "memory-bank/foo.md")
|
|
38
|
-
* to its absolute physical location on disk, respecting the IRIS source authority.
|
|
39
|
-
*/
|
|
40
|
-
export function resolveArtifactPath(repoRoot, relativePath) {
|
|
41
|
-
if (path.isAbsolute(relativePath)) {
|
|
42
|
-
return relativePath;
|
|
43
|
-
}
|
|
44
|
-
if (relativePath.startsWith(".iris/") || relativePath === ".iris") {
|
|
45
|
-
const root = resolveIrisRoot(repoRoot);
|
|
46
|
-
if (relativePath === ".iris")
|
|
47
|
-
return root.path;
|
|
48
|
-
// internal path inside .iris
|
|
49
|
-
const subPath = relativePath.substring(6); // remove ".iris/"
|
|
50
|
-
return path.join(root.path, subPath);
|
|
51
|
-
}
|
|
52
|
-
// Standard repo file
|
|
53
|
-
return path.join(repoRoot, relativePath);
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Debug helper to show resolution status
|
|
57
|
-
*/
|
|
58
|
-
export function getIrisDebugInfo(repoRoot) {
|
|
59
|
-
const root = resolveIrisRoot(repoRoot);
|
|
60
|
-
return {
|
|
61
|
-
repoRoot,
|
|
62
|
-
irisRoot: root.path,
|
|
63
|
-
source: root.source,
|
|
64
|
-
bundledPath: path.resolve(__dirname, "../iris_bundle/.iris")
|
|
65
|
-
};
|
|
66
|
-
}
|
package/dist/iris/router.js
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
export function routeIntent(intent, routesConfig) {
|
|
2
|
-
const candidates = [];
|
|
3
|
-
for (const route of routesConfig.routes) {
|
|
4
|
-
// 1. Check exclusions
|
|
5
|
-
if (checkExclusion(intent, route.exclude)) {
|
|
6
|
-
continue;
|
|
7
|
-
}
|
|
8
|
-
// 2. Score Match
|
|
9
|
-
const { score, matches } = scoreRoute(intent, route);
|
|
10
|
-
// Qualification:
|
|
11
|
-
if (score > 0) {
|
|
12
|
-
candidates.push({ route, score, matches });
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
// Sort: 1. Score desc, 2. Longest match count desc, 3. Route order (stable sort)
|
|
16
|
-
candidates.sort((a, b) => {
|
|
17
|
-
if (b.score !== a.score)
|
|
18
|
-
return b.score - a.score;
|
|
19
|
-
if (b.matches.length !== a.matches.length)
|
|
20
|
-
return b.matches.length - a.matches.length;
|
|
21
|
-
return 0;
|
|
22
|
-
});
|
|
23
|
-
if (candidates.length > 0) {
|
|
24
|
-
return {
|
|
25
|
-
route: candidates[0].route,
|
|
26
|
-
score: candidates[0].score,
|
|
27
|
-
matches: candidates[0].matches
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
// Default
|
|
31
|
-
return {
|
|
32
|
-
route: "default",
|
|
33
|
-
score: 0,
|
|
34
|
-
matches: []
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
function checkExclusion(intent, exclude) {
|
|
38
|
-
if (!exclude)
|
|
39
|
-
return false;
|
|
40
|
-
const lower = intent.toLowerCase();
|
|
41
|
-
// Any / Any Keywords
|
|
42
|
-
const anyList = [...(exclude.any || []), ...(exclude.any_keywords || [])];
|
|
43
|
-
if (anyList.length > 0) {
|
|
44
|
-
for (const kw of anyList) {
|
|
45
|
-
if (lower.includes(kw.toLowerCase()))
|
|
46
|
-
return true;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
// Regex
|
|
50
|
-
if (exclude.regex) {
|
|
51
|
-
for (const pattern of exclude.regex) {
|
|
52
|
-
try {
|
|
53
|
-
if (new RegExp(pattern, "i").test(intent))
|
|
54
|
-
return true;
|
|
55
|
-
}
|
|
56
|
-
catch (e) {
|
|
57
|
-
console.warn(`Invalid exclude regex: ${pattern}`);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
return false;
|
|
62
|
-
}
|
|
63
|
-
function scoreRoute(intent, route) {
|
|
64
|
-
let score = 0;
|
|
65
|
-
const matches = [];
|
|
66
|
-
const lower = intent.toLowerCase();
|
|
67
|
-
const rules = route.match;
|
|
68
|
-
// 1. Check ALL
|
|
69
|
-
const allList = [...(rules.all || []), ...(rules.all_keywords || [])];
|
|
70
|
-
if (allList.length > 0) {
|
|
71
|
-
for (const kw of allList) {
|
|
72
|
-
if (lower.includes(kw.toLowerCase())) {
|
|
73
|
-
score += 1;
|
|
74
|
-
matches.push(`all:${kw}`);
|
|
75
|
-
}
|
|
76
|
-
else {
|
|
77
|
-
return { score: 0, matches: [] }; // Failed ALL condition
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
// 2. Check ANY + Phrases
|
|
82
|
-
const anyList = [
|
|
83
|
-
...(rules.any || []),
|
|
84
|
-
...(rules.any_keywords || []),
|
|
85
|
-
...(rules.phrases || [])
|
|
86
|
-
];
|
|
87
|
-
if (anyList.length > 0) {
|
|
88
|
-
for (const kw of anyList) {
|
|
89
|
-
if (lower.includes(kw.toLowerCase())) {
|
|
90
|
-
score += 1;
|
|
91
|
-
matches.push(`any:${kw}`);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
// 3. Check Regex
|
|
96
|
-
if (rules.regex) {
|
|
97
|
-
for (const pattern of rules.regex) {
|
|
98
|
-
try {
|
|
99
|
-
if (new RegExp(pattern, "i").test(intent)) {
|
|
100
|
-
score += 2;
|
|
101
|
-
matches.push(`regex:${pattern}`);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
catch (e) {
|
|
105
|
-
console.warn(`Invalid match regex in route ${route.route_id}: ${pattern}`);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
// Boost
|
|
110
|
-
if (score > 0 && route.boost) {
|
|
111
|
-
score += route.boost;
|
|
112
|
-
}
|
|
113
|
-
return { score, matches };
|
|
114
|
-
}
|
package/dist/iris/routes.js
DELETED
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import yaml from "js-yaml";
|
|
4
|
-
import { repoRoot } from "../lib.js";
|
|
5
|
-
import { resolveArtifactPath } from "./resolver.js";
|
|
6
|
-
import kleur from "kleur";
|
|
7
|
-
// --- Errors ---
|
|
8
|
-
export class RoutesError extends Error {
|
|
9
|
-
constructor(message) {
|
|
10
|
-
super(message);
|
|
11
|
-
this.name = "RoutesError";
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
export class RoutesLoadError extends RoutesError {
|
|
15
|
-
cause;
|
|
16
|
-
constructor(message, cause) {
|
|
17
|
-
super(message);
|
|
18
|
-
this.cause = cause;
|
|
19
|
-
this.name = "RoutesLoadError";
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
export class RoutesOverlayMissingError extends RoutesError {
|
|
23
|
-
constructor(message) {
|
|
24
|
-
super(message);
|
|
25
|
-
this.name = "RoutesOverlayMissingError";
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
// --- Loaders ---
|
|
29
|
-
export function loadBaseRoutes(framework, root) {
|
|
30
|
-
try {
|
|
31
|
-
const r = root || repoRoot();
|
|
32
|
-
// 1. Framework Priority
|
|
33
|
-
if (framework && framework.files.routes && fs.existsSync(framework.files.routes)) {
|
|
34
|
-
const content = fs.readFileSync(framework.files.routes, "utf8");
|
|
35
|
-
const doc = yaml.load(content);
|
|
36
|
-
validateRoutes(doc, "framework-base");
|
|
37
|
-
validateRoutes(doc, "framework-base");
|
|
38
|
-
doc.routes.forEach(r => r._source = "base");
|
|
39
|
-
Object.defineProperty(doc, '_meta', {
|
|
40
|
-
value: { sourceKind: "framework", sourcePath: framework.files.routes },
|
|
41
|
-
enumerable: false,
|
|
42
|
-
writable: true
|
|
43
|
-
});
|
|
44
|
-
return doc;
|
|
45
|
-
}
|
|
46
|
-
// 2. Legacy Fallback
|
|
47
|
-
const legacyPath = resolveArtifactPath(r, ".iris/routes.yaml");
|
|
48
|
-
if (fs.existsSync(legacyPath)) {
|
|
49
|
-
if (framework) {
|
|
50
|
-
console.error(kleur.yellow(`IRIS_WARNING IRIS_DEPRECATED_LEGACY_ROUTES: framework=${framework.manifest.id} missing routes.yaml; using .iris/routes.yaml`));
|
|
51
|
-
}
|
|
52
|
-
const content = fs.readFileSync(legacyPath, "utf8");
|
|
53
|
-
const doc = yaml.load(content);
|
|
54
|
-
validateRoutes(doc, "legacy-base");
|
|
55
|
-
doc.routes.forEach(r => r._source = "base");
|
|
56
|
-
Object.defineProperty(doc, '_meta', {
|
|
57
|
-
value: { sourceKind: "legacy", sourcePath: legacyPath },
|
|
58
|
-
enumerable: false,
|
|
59
|
-
writable: true
|
|
60
|
-
});
|
|
61
|
-
return doc;
|
|
62
|
-
}
|
|
63
|
-
throw new RoutesLoadError(`Missing required routes.yaml (Checked framework '${framework?.manifest.id || 'none'}' and legacy .iris/routes.yaml)`);
|
|
64
|
-
}
|
|
65
|
-
catch (error) {
|
|
66
|
-
if (error instanceof RoutesError)
|
|
67
|
-
throw error;
|
|
68
|
-
throw new RoutesLoadError("Failed to load base routes", error);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
export const loadRoutes = (root) => loadBaseRoutes(null, root);
|
|
72
|
-
/**
|
|
73
|
-
* Loads Effective Routes: Base + User Repo Overlay.
|
|
74
|
-
*/
|
|
75
|
-
export function loadEffectiveRoutes(framework, root, activeFlowId) {
|
|
76
|
-
// 1. Load Base (Framework or Legacy)
|
|
77
|
-
// Note: base is required. loadBaseRoutes throws if missing.
|
|
78
|
-
const r = root || repoRoot();
|
|
79
|
-
const base = loadBaseRoutes(framework, r);
|
|
80
|
-
// Determine Overlay Namespace: Flow ID (if active) OR Framework ID (if loaded)
|
|
81
|
-
const overlayNamespace = activeFlowId || framework?.manifest.id;
|
|
82
|
-
if (!overlayNamespace)
|
|
83
|
-
return base;
|
|
84
|
-
try {
|
|
85
|
-
// User Repo Overlay: .iris/overlays/<namespace>/routes.yaml
|
|
86
|
-
// This allows the user to override routing via flow OR framework namespace.
|
|
87
|
-
const overlayPath = path.join(r, ".iris/overlays", overlayNamespace, "routes.yaml");
|
|
88
|
-
if (fs.existsSync(overlayPath)) {
|
|
89
|
-
const content = fs.readFileSync(overlayPath, "utf8");
|
|
90
|
-
const overlay = (yaml.load(content) || {});
|
|
91
|
-
const merged = mergeRoutes(base, overlay);
|
|
92
|
-
Object.defineProperty(merged, '_meta', {
|
|
93
|
-
value: {
|
|
94
|
-
sourceKind: "mixed",
|
|
95
|
-
sourcePath: overlayPath
|
|
96
|
-
},
|
|
97
|
-
enumerable: false,
|
|
98
|
-
writable: true
|
|
99
|
-
});
|
|
100
|
-
return merged;
|
|
101
|
-
}
|
|
102
|
-
// Return base if no overlay found (Optional)
|
|
103
|
-
// Ensure we throw if explicit namespace was requested but not found?
|
|
104
|
-
// The test expects RoutesOverlayMissingError if "missing" flow is passed.
|
|
105
|
-
// Assuming if overlayNamespace is provided, we EXPECT an overlay.
|
|
106
|
-
if (overlayNamespace) {
|
|
107
|
-
// Only throw if this was an EXPLICIT flow request (activeFlowId is set)
|
|
108
|
-
// If it fell back to framework ID, treat overlay as optional.
|
|
109
|
-
if (activeFlowId) {
|
|
110
|
-
throw new RoutesOverlayMissingError(`Routes overlay not found for namespace '${overlayNamespace}' at ${overlayPath}`);
|
|
111
|
-
}
|
|
112
|
-
// else: explicit overlay not found for base framework -> return base (safe)
|
|
113
|
-
}
|
|
114
|
-
return base;
|
|
115
|
-
}
|
|
116
|
-
catch (error) {
|
|
117
|
-
if (error instanceof RoutesError)
|
|
118
|
-
throw error;
|
|
119
|
-
throw new RoutesLoadError(`Failed to load effective routes for namespace '${overlayNamespace}'`, error);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
/**
|
|
123
|
-
* Merges routes purely.
|
|
124
|
-
*/
|
|
125
|
-
export function mergeRoutes(base, overlay) {
|
|
126
|
-
// Deep clone base
|
|
127
|
-
const result = JSON.parse(JSON.stringify(base));
|
|
128
|
-
// 1. Merge Default
|
|
129
|
-
if (overlay.default) {
|
|
130
|
-
result.default = { ...result.default, ...overlay.default };
|
|
131
|
-
}
|
|
132
|
-
if (!overlay.routes || !Array.isArray(overlay.routes)) {
|
|
133
|
-
return result;
|
|
134
|
-
}
|
|
135
|
-
// Validate Overlay Routes first
|
|
136
|
-
overlay.routes.forEach(r => validateRoute(r, "overlay"));
|
|
137
|
-
// 2. Merge Routes
|
|
138
|
-
const baseMap = new Map();
|
|
139
|
-
result.routes.forEach((r, idx) => baseMap.set(r.route_id, idx));
|
|
140
|
-
const newRoutes = [];
|
|
141
|
-
for (const oRoute of overlay.routes) {
|
|
142
|
-
if (baseMap.has(oRoute.route_id)) {
|
|
143
|
-
// Override
|
|
144
|
-
const idx = baseMap.get(oRoute.route_id);
|
|
145
|
-
result.routes[idx] = {
|
|
146
|
-
...result.routes[idx],
|
|
147
|
-
...oRoute,
|
|
148
|
-
_source: "merged"
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
else {
|
|
152
|
-
// New
|
|
153
|
-
newRoutes.push({
|
|
154
|
-
...oRoute,
|
|
155
|
-
_source: "overlay"
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
// Prepend new routes
|
|
160
|
-
if (newRoutes.length > 0) {
|
|
161
|
-
result.routes.unshift(...newRoutes);
|
|
162
|
-
}
|
|
163
|
-
// Validate Final Result (Uniqueness check)
|
|
164
|
-
validateRoutes(result, "effective");
|
|
165
|
-
return result;
|
|
166
|
-
}
|
|
167
|
-
// --- Validation Helpers ---
|
|
168
|
-
function validateRoutes(routes, context) {
|
|
169
|
-
if (!routes || !routes.routes) {
|
|
170
|
-
throw new RoutesLoadError(`Invalid routes format (${context}): 'routes' array missing.`);
|
|
171
|
-
}
|
|
172
|
-
const seenIds = new Set();
|
|
173
|
-
routes.routes.forEach(r => {
|
|
174
|
-
validateRoute(r, context);
|
|
175
|
-
if (seenIds.has(r.route_id)) {
|
|
176
|
-
throw new RoutesLoadError(`Duplicate route_id found (${context}): '${r.route_id}'`);
|
|
177
|
-
}
|
|
178
|
-
seenIds.add(r.route_id);
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
function validateRoute(r, context) {
|
|
182
|
-
if (!r.route_id || typeof r.route_id !== "string" || !r.route_id.trim()) {
|
|
183
|
-
throw new RoutesLoadError(`Invalid route in ${context}: route_id must be a non-empty string.`);
|
|
184
|
-
}
|
|
185
|
-
// Normalize Phase
|
|
186
|
-
if (r.phase) {
|
|
187
|
-
r.phase = r.phase.toLowerCase();
|
|
188
|
-
}
|
|
189
|
-
}
|
package/dist/iris/run-state.js
DELETED
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import { randomUUID } from "crypto";
|
|
4
|
-
import { WorkflowStage } from "../bridge/types.js";
|
|
5
|
-
const RUNS_DIR = path.join(process.cwd(), ".iris/runs");
|
|
6
|
-
import { generateIntentId } from "./utils/interpolate.js";
|
|
7
|
-
/**
|
|
8
|
-
* Create a new workflow run
|
|
9
|
-
*/
|
|
10
|
-
export function createRun(intent, ideId, gateMode = "manual") {
|
|
11
|
-
const runId = randomUUID();
|
|
12
|
-
const now = new Date().toISOString();
|
|
13
|
-
const intentId = generateIntentId({ goal: intent }); // Fallback to intent processing if draft is missing
|
|
14
|
-
const runState = {
|
|
15
|
-
runId,
|
|
16
|
-
intent,
|
|
17
|
-
intentId,
|
|
18
|
-
ideId,
|
|
19
|
-
stage: WorkflowStage.INTENT_INCEPTION,
|
|
20
|
-
gateMode,
|
|
21
|
-
artifacts: [],
|
|
22
|
-
bolts: [],
|
|
23
|
-
currentBoltIndex: 0,
|
|
24
|
-
pendingTasks: [],
|
|
25
|
-
events: [],
|
|
26
|
-
createdAt: now,
|
|
27
|
-
updatedAt: now
|
|
28
|
-
};
|
|
29
|
-
saveRun(runState);
|
|
30
|
-
return runState;
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* Load a run by ID
|
|
34
|
-
*/
|
|
35
|
-
export function loadRun(runId) {
|
|
36
|
-
const runPath = path.join(RUNS_DIR, `${runId}.json`);
|
|
37
|
-
if (!fs.existsSync(runPath)) {
|
|
38
|
-
throw new Error(`Run not found: ${runId}`);
|
|
39
|
-
}
|
|
40
|
-
const content = fs.readFileSync(runPath, "utf8");
|
|
41
|
-
return JSON.parse(content);
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Save run state
|
|
45
|
-
*/
|
|
46
|
-
export function saveRun(state) {
|
|
47
|
-
if (!fs.existsSync(RUNS_DIR)) {
|
|
48
|
-
fs.mkdirSync(RUNS_DIR, { recursive: true });
|
|
49
|
-
}
|
|
50
|
-
state.updatedAt = new Date().toISOString();
|
|
51
|
-
const runPath = path.join(RUNS_DIR, `${state.runId}.json`);
|
|
52
|
-
fs.writeFileSync(runPath, JSON.stringify(state, null, 2), "utf8");
|
|
53
|
-
// Also save a small state snapshot for quick resume
|
|
54
|
-
const statePath = path.join(RUNS_DIR, `${state.runId}.state.json`);
|
|
55
|
-
const snapshot = {
|
|
56
|
-
runId: state.runId,
|
|
57
|
-
stage: state.stage,
|
|
58
|
-
currentBoltIndex: state.currentBoltIndex,
|
|
59
|
-
updatedAt: state.updatedAt
|
|
60
|
-
};
|
|
61
|
-
fs.writeFileSync(statePath, JSON.stringify(snapshot, null, 2), "utf8");
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* Append an event to the run
|
|
65
|
-
*/
|
|
66
|
-
export function appendEvent(runId, event) {
|
|
67
|
-
const state = loadRun(runId);
|
|
68
|
-
const fullEvent = {
|
|
69
|
-
...event,
|
|
70
|
-
timestamp: new Date().toISOString()
|
|
71
|
-
};
|
|
72
|
-
state.events.push(fullEvent);
|
|
73
|
-
saveRun(state);
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* Generate a human-readable summary of the run
|
|
77
|
-
*/
|
|
78
|
-
export function generateRunSummary(runId) {
|
|
79
|
-
const state = loadRun(runId);
|
|
80
|
-
let summary = `# IRIS Workflow Run: ${runId}\n\n`;
|
|
81
|
-
summary += `**Intent:** ${state.intent}\n`;
|
|
82
|
-
summary += `**IDE:** ${state.ideId}\n`;
|
|
83
|
-
summary += `**Stage:** ${state.stage}\n`;
|
|
84
|
-
summary += `**Created:** ${state.createdAt}\n`;
|
|
85
|
-
summary += `**Updated:** ${state.updatedAt}\n\n`;
|
|
86
|
-
summary += `## Artifacts Created\n\n`;
|
|
87
|
-
if (state.artifacts.length > 0) {
|
|
88
|
-
summary += state.artifacts.map(a => `- \`${a}\``).join("\n") + "\n\n";
|
|
89
|
-
}
|
|
90
|
-
else {
|
|
91
|
-
summary += "*No artifacts yet*\n\n";
|
|
92
|
-
}
|
|
93
|
-
summary += `## Bolts\n\n`;
|
|
94
|
-
if (state.bolts.length > 0) {
|
|
95
|
-
state.bolts.forEach(bolt => {
|
|
96
|
-
summary += `### ${bolt.id} (${bolt.status})\n`;
|
|
97
|
-
if (bolt.designCompleted)
|
|
98
|
-
summary += "- ✓ Design\n";
|
|
99
|
-
if (bolt.implementationCompleted)
|
|
100
|
-
summary += "- ✓ Implementation\n";
|
|
101
|
-
if (bolt.testingCompleted)
|
|
102
|
-
summary += "- ✓ Testing\n";
|
|
103
|
-
summary += "\n";
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
else {
|
|
107
|
-
summary += "*No bolts yet*\n\n";
|
|
108
|
-
}
|
|
109
|
-
summary += `## Event Log\n\n`;
|
|
110
|
-
state.events.forEach(event => {
|
|
111
|
-
summary += `**${event.timestamp}** [${event.stage}] ${event.action}\n`;
|
|
112
|
-
if (event.message)
|
|
113
|
-
summary += ` ${event.message}\n`;
|
|
114
|
-
if (event.userGateDecision)
|
|
115
|
-
summary += ` Gate: ${event.userGateDecision}\n`;
|
|
116
|
-
summary += "\n";
|
|
117
|
-
});
|
|
118
|
-
return summary;
|
|
119
|
-
}
|
|
120
|
-
/**
|
|
121
|
-
* Save run summary to markdown file
|
|
122
|
-
*/
|
|
123
|
-
export function saveRunSummary(runId) {
|
|
124
|
-
const summary = generateRunSummary(runId);
|
|
125
|
-
const summaryPath = path.join(RUNS_DIR, `${runId}.md`);
|
|
126
|
-
fs.writeFileSync(summaryPath, summary, "utf8");
|
|
127
|
-
}
|
|
128
|
-
/**
|
|
129
|
-
* List all runs
|
|
130
|
-
*/
|
|
131
|
-
export function listRuns() {
|
|
132
|
-
if (!fs.existsSync(RUNS_DIR)) {
|
|
133
|
-
return [];
|
|
134
|
-
}
|
|
135
|
-
const files = fs.readdirSync(RUNS_DIR);
|
|
136
|
-
const stateFiles = files.filter(f => f.endsWith(".state.json"));
|
|
137
|
-
return stateFiles.map(file => {
|
|
138
|
-
const content = fs.readFileSync(path.join(RUNS_DIR, file), "utf8");
|
|
139
|
-
const snapshot = JSON.parse(content);
|
|
140
|
-
return {
|
|
141
|
-
runId: snapshot.runId,
|
|
142
|
-
stage: snapshot.stage,
|
|
143
|
-
updatedAt: snapshot.updatedAt
|
|
144
|
-
};
|
|
145
|
-
}).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
146
|
-
}
|
package/dist/iris/state.js
DELETED
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import yaml from "js-yaml";
|
|
4
|
-
import { EXIT_CODES } from "../utils/exit-codes.js";
|
|
5
|
-
const STATE_FILE_PATH = path.join(process.cwd(), ".iris/state.yaml");
|
|
6
|
-
export const DEFAULT_STATE = {
|
|
7
|
-
version: 1,
|
|
8
|
-
framework: {
|
|
9
|
-
current: "iris-core",
|
|
10
|
-
version: null,
|
|
11
|
-
},
|
|
12
|
-
phase: {
|
|
13
|
-
current: "inception",
|
|
14
|
-
requested: null,
|
|
15
|
-
},
|
|
16
|
-
active: {
|
|
17
|
-
intent_id: null,
|
|
18
|
-
unit_id: null,
|
|
19
|
-
bolt_id: null,
|
|
20
|
-
flow: null,
|
|
21
|
-
},
|
|
22
|
-
last_validation: {
|
|
23
|
-
at: null,
|
|
24
|
-
result: null,
|
|
25
|
-
},
|
|
26
|
-
};
|
|
27
|
-
export function loadState() {
|
|
28
|
-
try {
|
|
29
|
-
if (!fs.existsSync(STATE_FILE_PATH)) {
|
|
30
|
-
// Auto-create directory if it doesn't exist
|
|
31
|
-
const dir = path.dirname(STATE_FILE_PATH);
|
|
32
|
-
if (!fs.existsSync(dir)) {
|
|
33
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
34
|
-
}
|
|
35
|
-
saveState(DEFAULT_STATE);
|
|
36
|
-
return DEFAULT_STATE;
|
|
37
|
-
}
|
|
38
|
-
const content = fs.readFileSync(STATE_FILE_PATH, "utf8");
|
|
39
|
-
const doc = yaml.load(content);
|
|
40
|
-
// Start with defaults to ensure all fields exist (deep merge strategy)
|
|
41
|
-
// We manually merge top-level objects to avoid losing nested defaults
|
|
42
|
-
const merged = {
|
|
43
|
-
...DEFAULT_STATE,
|
|
44
|
-
...doc,
|
|
45
|
-
framework: { ...DEFAULT_STATE.framework, ...doc.framework },
|
|
46
|
-
phase: { ...DEFAULT_STATE.phase, ...doc.phase },
|
|
47
|
-
active: { ...DEFAULT_STATE.active, ...doc.active },
|
|
48
|
-
last_validation: { ...DEFAULT_STATE.last_validation, ...doc.last_validation },
|
|
49
|
-
// Optional fields
|
|
50
|
-
ide: doc.ide ? { ...doc.ide } : undefined,
|
|
51
|
-
bridge: doc.bridge ? { ...doc.bridge } : undefined,
|
|
52
|
-
};
|
|
53
|
-
// Migration/Fixup for legacy/bundled schema
|
|
54
|
-
if (doc.current_phase && !doc.phase) {
|
|
55
|
-
merged.phase = {
|
|
56
|
-
current: doc.current_phase.toLowerCase(), // Normalize to lowercase
|
|
57
|
-
requested: null
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
// Ensure flow exists (backward compatibility)
|
|
61
|
-
if (merged.active.flow === undefined)
|
|
62
|
-
merged.active.flow = null;
|
|
63
|
-
return merged;
|
|
64
|
-
}
|
|
65
|
-
catch (error) {
|
|
66
|
-
console.error("Failed to load or create .iris/state.yaml:", error);
|
|
67
|
-
process.exit(EXIT_CODES.STATE_ERROR);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
export function saveState(state) {
|
|
71
|
-
try {
|
|
72
|
-
const content = yaml.dump(state);
|
|
73
|
-
fs.writeFileSync(STATE_FILE_PATH, content, "utf8");
|
|
74
|
-
}
|
|
75
|
-
catch (error) {
|
|
76
|
-
console.error("Failed to write .iris/state.yaml:", error);
|
|
77
|
-
process.exit(EXIT_CODES.STATE_ERROR);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* Get the selected IDE from state, or null if not set
|
|
82
|
-
*/
|
|
83
|
-
export function getSelectedIde() {
|
|
84
|
-
const state = loadState();
|
|
85
|
-
return state.ide?.id || null;
|
|
86
|
-
}
|
|
87
|
-
/**
|
|
88
|
-
* Set the selected IDE in state
|
|
89
|
-
*/
|
|
90
|
-
export function setSelectedIde(ideId) {
|
|
91
|
-
const state = loadState();
|
|
92
|
-
state.ide = {
|
|
93
|
-
id: ideId,
|
|
94
|
-
selected_at: new Date().toISOString()
|
|
95
|
-
};
|
|
96
|
-
saveState(state);
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* Get the active framework info
|
|
100
|
-
*/
|
|
101
|
-
export function getActiveFramework(state = loadState()) {
|
|
102
|
-
return state.framework;
|
|
103
|
-
}
|
|
104
|
-
/**
|
|
105
|
-
* Set the active framework
|
|
106
|
-
*/
|
|
107
|
-
export function setActiveFramework(state, id, version = null) {
|
|
108
|
-
state.framework = {
|
|
109
|
-
current: id,
|
|
110
|
-
version: version
|
|
111
|
-
};
|
|
112
|
-
saveState(state);
|
|
113
|
-
}
|
package/dist/iris/templates.js
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
export const UNIT_BRIEF_TEMPLATE = `---
|
|
2
|
-
id: {{unitId}}
|
|
3
|
-
status: draft
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Unit Brief: {{title}}
|
|
7
|
-
|
|
8
|
-
## Objective
|
|
9
|
-
{{summary}}
|
|
10
|
-
|
|
11
|
-
## Scope
|
|
12
|
-
### In Scope
|
|
13
|
-
- [ ] Implementation item 1
|
|
14
|
-
- [ ] Implementation item 2
|
|
15
|
-
|
|
16
|
-
### Out of Scope
|
|
17
|
-
- Non-goals for this unit
|
|
18
|
-
|
|
19
|
-
## Acceptance Criteria
|
|
20
|
-
- [ ] Criteria 1
|
|
21
|
-
- [ ] Criteria 2
|
|
22
|
-
|
|
23
|
-
## Risks
|
|
24
|
-
- Risk 1
|
|
25
|
-
`;
|
|
26
|
-
export const BOLT_TEMPLATE = `---
|
|
27
|
-
id: {{boltId}}
|
|
28
|
-
unit: {{unitId}}
|
|
29
|
-
status: draft
|
|
30
|
-
type: feature
|
|
31
|
-
files:
|
|
32
|
-
- src/index.ts
|
|
33
|
-
cwd: .
|
|
34
|
-
commands:
|
|
35
|
-
test: npm test
|
|
36
|
-
---
|
|
37
|
-
|
|
38
|
-
# Bolt: {{title}}
|
|
39
|
-
|
|
40
|
-
## Implementation Plan
|
|
41
|
-
- [ ] Step 1
|
|
42
|
-
- [ ] Step 2
|
|
43
|
-
|
|
44
|
-
## Files to Touch
|
|
45
|
-
- src/index.ts
|
|
46
|
-
|
|
47
|
-
## Verification Plan
|
|
48
|
-
### Automated Tests
|
|
49
|
-
- \`npm test\`
|
|
50
|
-
|
|
51
|
-
### Manual Verification
|
|
52
|
-
- Check output
|
|
53
|
-
`;
|
|
54
|
-
export const TEST_REPORT_TEMPLATE = `---
|
|
55
|
-
bolt: {{boltId}}
|
|
56
|
-
status: {{status}}
|
|
57
|
-
timestamp: {{timestamp}}
|
|
58
|
-
---
|
|
59
|
-
|
|
60
|
-
# Test Report: {{title}}
|
|
61
|
-
|
|
62
|
-
## Summary
|
|
63
|
-
Status: {{status}}
|
|
64
|
-
Time: {{timestamp}}
|
|
65
|
-
|
|
66
|
-
## Output
|
|
67
|
-
\`\`\`
|
|
68
|
-
{{output}}
|
|
69
|
-
\`\`\`
|
|
70
|
-
`;
|