@jstn-sdk/ma 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/plugins/marketplace.json +20 -0
- package/.codex/agents/Architect.toml +4 -0
- package/.codex/agents/Auditor.toml +4 -0
- package/.codex/agents/Builder.toml +4 -0
- package/.codex/agents/Flow.toml +4 -0
- package/.codex/agents/Sage.toml +4 -0
- package/.codex/agents/Vibe.toml +4 -0
- package/.codex/hooks.json +15 -0
- package/.codex/prompts/enforcement.md +59 -0
- package/.codex/prompts/onboarding.md +41 -0
- package/.codex/prompts/release-rules.md +28 -0
- package/.codex/prompts/skill-contract.md +41 -0
- package/LICENSE +21 -0
- package/README.md +532 -0
- package/bin/ma.js +300 -0
- package/bin/meta-architect.js +3 -0
- package/docs/README.md +24 -0
- package/docs/assets/meta-architect-logo.png +0 -0
- package/docs/assets/meta-architect-logo.svg +8 -0
- package/docs/getting-started.md +451 -0
- package/docs/mcp-setup.md +5 -0
- package/docs/onboarding.md +65 -0
- package/docs/qa/release-readiness-0.1.0.md +62 -0
- package/docs/release-spec.md +86 -0
- package/docs/skills-publishing.md +231 -0
- package/docs/skills.md +91 -0
- package/index.js +28 -0
- package/mcp/collections.json +21 -0
- package/mcp/fallback.json +7 -0
- package/mcp/servers.json +55 -0
- package/package.json +84 -0
- package/plugins/meta-architect/.app.json +8 -0
- package/plugins/meta-architect/.codex-plugin/plugin.json +23 -0
- package/plugins/meta-architect/.mcp.json +12 -0
- package/plugins/meta-architect/README.md +121 -0
- package/plugins/meta-architect/skills/arch/SKILL.md +27 -0
- package/plugins/meta-architect/skills/arch/agents/openai.yaml +4 -0
- package/plugins/meta-architect/skills/build/SKILL.md +24 -0
- package/plugins/meta-architect/skills/build/agents/openai.yaml +4 -0
- package/plugins/meta-architect/skills/flow/SKILL.md +24 -0
- package/plugins/meta-architect/skills/flow/agents/openai.yaml +4 -0
- package/plugins/meta-architect/skills/meta-architect/SKILL.md +30 -0
- package/plugins/meta-architect/skills/meta-architect/agents/openai.yaml +4 -0
- package/plugins/meta-architect/skills/meta-architect/references/core-release-rules.md +13 -0
- package/plugins/meta-architect/skills/sage/SKILL.md +24 -0
- package/plugins/meta-architect/skills/sage/agents/openai.yaml +4 -0
- package/plugins/meta-architect/skills/vet/SKILL.md +25 -0
- package/plugins/meta-architect/skills/vet/agents/openai.yaml +4 -0
- package/plugins/meta-architect/skills/vibe/SKILL.md +24 -0
- package/plugins/meta-architect/skills/vibe/agents/openai.yaml +4 -0
- package/scripts/doctor.js +37 -0
- package/scripts/plugin-sync.js +92 -0
- package/scripts/postinstall.js +19 -0
- package/scripts/release-metadata.js +94 -0
- package/scripts/release-verify.js +153 -0
- package/scripts/setup-npmrc.js +39 -0
- package/scripts/skills-install.js +29 -0
- package/scripts/skills-manifest.js +61 -0
- package/scripts/skills-pack.js +54 -0
- package/scripts/skills-validate.js +118 -0
- package/skills/arch/SKILL.md +27 -0
- package/skills/arch/agents/openai.yaml +4 -0
- package/skills/build/SKILL.md +24 -0
- package/skills/build/agents/openai.yaml +4 -0
- package/skills/flow/SKILL.md +24 -0
- package/skills/flow/agents/openai.yaml +4 -0
- package/skills/index.json +40 -0
- package/skills/meta-architect/SKILL.md +30 -0
- package/skills/meta-architect/agents/openai.yaml +4 -0
- package/skills/meta-architect/references/core-release-rules.md +13 -0
- package/skills/sage/SKILL.md +24 -0
- package/skills/sage/agents/openai.yaml +4 -0
- package/skills/vet/SKILL.md +25 -0
- package/skills/vet/agents/openai.yaml +4 -0
- package/skills/vibe/SKILL.md +24 -0
- package/skills/vibe/agents/openai.yaml +4 -0
- package/sprint/00-idea.md +27 -0
- package/sprint/01-architecture.md +26 -0
- package/sprint/02-oss-evidence.md +26 -0
- package/sprint/03-logic.md +25 -0
- package/sprint/04-security.md +25 -0
- package/sprint/05-dx-ux.md +24 -0
- package/sprint/06-build-plan.md +26 -0
- package/sprint/07-release.md +26 -0
- package/src/build-gate.js +52 -0
- package/src/decision-log.js +40 -0
- package/src/fs-utils.js +25 -0
- package/src/launcher.js +55 -0
- package/src/mcp-config.js +30 -0
- package/src/mcp-live-client.js +186 -0
- package/src/paths.js +26 -0
- package/src/policy.js +23 -0
- package/src/release-state.js +59 -0
- package/src/runtime-artifacts.js +363 -0
- package/src/skill-installer.js +49 -0
- package/src/skills.js +507 -0
- package/src/state-sync.js +15 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Sprint 4: Security
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Record a baseline security pass and keep the workflow blocked if prerequisites are not satisfied.
|
|
6
|
+
|
|
7
|
+
## Inputs
|
|
8
|
+
|
|
9
|
+
- `logic_status = GREEN`
|
|
10
|
+
|
|
11
|
+
## Expected outputs
|
|
12
|
+
|
|
13
|
+
- `.ma/evidence/audits.json`
|
|
14
|
+
- `.ma/evidence/cves.json`
|
|
15
|
+
- `security_status = GREEN | RED`
|
|
16
|
+
|
|
17
|
+
## Exit criteria
|
|
18
|
+
|
|
19
|
+
- baseline security findings are recorded
|
|
20
|
+
- blockers are explicit when the lane remains red
|
|
21
|
+
|
|
22
|
+
## Failure conditions
|
|
23
|
+
|
|
24
|
+
- logic not green
|
|
25
|
+
- missing evidence output
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Sprint 5: DX/UX
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Record a baseline developer and user experience pass before build unlock.
|
|
6
|
+
|
|
7
|
+
## Inputs
|
|
8
|
+
|
|
9
|
+
- `security_status = GREEN`
|
|
10
|
+
|
|
11
|
+
## Expected outputs
|
|
12
|
+
|
|
13
|
+
- `.ma/evidence/outcomes.json`
|
|
14
|
+
- `experience_status = GREEN | RED | WAIVED`
|
|
15
|
+
|
|
16
|
+
## Exit criteria
|
|
17
|
+
|
|
18
|
+
- baseline experience notes are recorded
|
|
19
|
+
- waiver path is explicit when used
|
|
20
|
+
|
|
21
|
+
## Failure conditions
|
|
22
|
+
|
|
23
|
+
- security not green
|
|
24
|
+
- no operator-facing or user-facing review output
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Sprint 6: Build Plan
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Evaluate all required gates and prepare bounded implementation work.
|
|
6
|
+
|
|
7
|
+
## Inputs
|
|
8
|
+
|
|
9
|
+
- required upstream gates green or waived per release policy
|
|
10
|
+
|
|
11
|
+
## Expected outputs
|
|
12
|
+
|
|
13
|
+
- blocked or ready decision entry
|
|
14
|
+
- `build_status = READY` on success
|
|
15
|
+
- suggested `feature/*` branches
|
|
16
|
+
- optional `git worktree add` commands
|
|
17
|
+
|
|
18
|
+
## Exit criteria
|
|
19
|
+
|
|
20
|
+
- build is either clearly blocked or clearly ready
|
|
21
|
+
- branch/worktree guidance is explicit when ready
|
|
22
|
+
|
|
23
|
+
## Failure conditions
|
|
24
|
+
|
|
25
|
+
- missing or red gates
|
|
26
|
+
- invalid release-state file
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Sprint 7: Release
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Move from completed bounded implementation to a controlled merge and release path.
|
|
6
|
+
|
|
7
|
+
## Inputs
|
|
8
|
+
|
|
9
|
+
- `build_status = READY | RUNNING`
|
|
10
|
+
- valid branch origin
|
|
11
|
+
|
|
12
|
+
## Expected outputs
|
|
13
|
+
|
|
14
|
+
- `merge_status = MERGED_TO_DEVELOPMENT`
|
|
15
|
+
- `release_status = SHIPPED_TO_PROD`
|
|
16
|
+
|
|
17
|
+
## Exit criteria
|
|
18
|
+
|
|
19
|
+
- merge follows `feature/* -> development`
|
|
20
|
+
- release follows `development|release/* -> prod`
|
|
21
|
+
|
|
22
|
+
## Failure conditions
|
|
23
|
+
|
|
24
|
+
- invalid merge target
|
|
25
|
+
- invalid release origin
|
|
26
|
+
- attempt to bypass protected release flow
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const gateChecks = [
|
|
2
|
+
["idea_status", "CLEAR", "ma idea"],
|
|
3
|
+
["architecture_status", "APPROVED", "$arch"],
|
|
4
|
+
["evidence_status", "VERIFIED", "$sage"],
|
|
5
|
+
["logic_status", "GREEN", "$flow"],
|
|
6
|
+
["security_status", "GREEN", "$vet"],
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
export function evaluateBuildGate(releaseState) {
|
|
10
|
+
const blockers = [];
|
|
11
|
+
|
|
12
|
+
for (const [field, expected, trigger] of gateChecks) {
|
|
13
|
+
if (releaseState[field] !== expected) {
|
|
14
|
+
blockers.push({ field, expected, actual: releaseState[field], trigger });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const experienceAllowed =
|
|
19
|
+
releaseState.experience_status === "GREEN" || releaseState.experience_status === "WAIVED";
|
|
20
|
+
if (!experienceAllowed) {
|
|
21
|
+
blockers.push({
|
|
22
|
+
field: "experience_status",
|
|
23
|
+
expected: "GREEN or WAIVED",
|
|
24
|
+
actual: releaseState.experience_status,
|
|
25
|
+
trigger: "$vibe",
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!["LOCKED", "READY"].includes(releaseState.build_status)) {
|
|
30
|
+
blockers.push({
|
|
31
|
+
field: "build_status",
|
|
32
|
+
expected: "LOCKED or READY",
|
|
33
|
+
actual: releaseState.build_status,
|
|
34
|
+
trigger: "$build",
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
allowed: blockers.length === 0,
|
|
40
|
+
blockers,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function formatBuildBlockers(evaluation) {
|
|
45
|
+
return evaluation.blockers.map(
|
|
46
|
+
(blocker) => `${blocker.field} must be ${blocker.expected}; current value is ${blocker.actual}`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function formatNextAllowedTriggers(evaluation) {
|
|
51
|
+
return [...new Set(evaluation.blockers.map((blocker) => blocker.trigger))];
|
|
52
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { readJson, writeJson } from "./fs-utils.js";
|
|
2
|
+
import { getRuntimeReadPath, getRuntimeWritePath } from "./paths.js";
|
|
3
|
+
|
|
4
|
+
function validateDecisionShape(entry) {
|
|
5
|
+
const required = ["decision", "status", "evidence", "blockers", "next_allowed_triggers"];
|
|
6
|
+
for (const field of required) {
|
|
7
|
+
if (!(field in entry)) {
|
|
8
|
+
throw new Error(`Decision entry missing field: ${field}`);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function loadDecisionLog() {
|
|
14
|
+
const parsed = await readJson(getRuntimeReadPath("decisions.json"));
|
|
15
|
+
|
|
16
|
+
if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.decisions)) {
|
|
17
|
+
throw new Error("Decision log must be an object with a decisions array");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return parsed;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function appendDecision(entry) {
|
|
24
|
+
validateDecisionShape(entry);
|
|
25
|
+
const log = await loadDecisionLog();
|
|
26
|
+
log.decisions.push({
|
|
27
|
+
...entry,
|
|
28
|
+
timestamp: new Date().toISOString(),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
await writeJson(getRuntimeWritePath("decisions.json"), log);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function updateDecisionStatuses(statusUpdates) {
|
|
35
|
+
const log = await loadDecisionLog();
|
|
36
|
+
for (const [field, value] of Object.entries(statusUpdates)) {
|
|
37
|
+
log[field] = value;
|
|
38
|
+
}
|
|
39
|
+
await writeJson(getRuntimeWritePath("decisions.json"), log);
|
|
40
|
+
}
|
package/src/fs-utils.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export async function readJson(filePath) {
|
|
5
|
+
const raw = await fs.readFile(filePath, "utf8");
|
|
6
|
+
return JSON.parse(raw);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function writeJson(filePath, value) {
|
|
10
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
11
|
+
await fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function ensureDir(dirPath) {
|
|
15
|
+
await fs.mkdir(dirPath, { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function writeFileIfMissing(filePath, content) {
|
|
19
|
+
try {
|
|
20
|
+
await fs.access(filePath);
|
|
21
|
+
} catch {
|
|
22
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
23
|
+
await fs.writeFile(filePath, content);
|
|
24
|
+
}
|
|
25
|
+
}
|
package/src/launcher.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
const nativeCommands = new Set([
|
|
5
|
+
"setup",
|
|
6
|
+
"init",
|
|
7
|
+
"idea",
|
|
8
|
+
"skills",
|
|
9
|
+
"status",
|
|
10
|
+
"merge",
|
|
11
|
+
"release",
|
|
12
|
+
"run",
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
export function shouldDelegateToCodex(args) {
|
|
16
|
+
if (args.length === 0) {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return !nativeCommands.has(args[0]);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function normalizeCodexArgs(args) {
|
|
24
|
+
return args.filter((arg) => arg !== "--madmax" && arg !== "--high");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function resolveCodexCommand() {
|
|
28
|
+
if (process.env.MA_CODEX_BIN) {
|
|
29
|
+
return process.env.MA_CODEX_BIN;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return "codex";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function runCodex(args) {
|
|
36
|
+
const codexCommand = resolveCodexCommand();
|
|
37
|
+
const codexArgs = normalizeCodexArgs(args);
|
|
38
|
+
const commandArgs =
|
|
39
|
+
path.extname(codexCommand) === ".js" || path.extname(codexCommand) === ".mjs"
|
|
40
|
+
? [codexCommand, ...codexArgs]
|
|
41
|
+
: codexArgs;
|
|
42
|
+
const command = commandArgs[0] === codexCommand ? process.execPath : codexCommand;
|
|
43
|
+
const finalArgs = command === process.execPath ? commandArgs : codexArgs;
|
|
44
|
+
const result = spawnSync(command, finalArgs, { stdio: "inherit" });
|
|
45
|
+
|
|
46
|
+
if (result.error) {
|
|
47
|
+
if (result.error.code === "ENOENT") {
|
|
48
|
+
throw new Error("Codex CLI not found. Install it with: npm install -g @openai/codex");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
throw new Error(`Failed to start Codex CLI: ${result.error.message}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return typeof result.status === "number" ? result.status : 1;
|
|
55
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import { getMcpServersPath } from "./paths.js";
|
|
3
|
+
|
|
4
|
+
const gitMcpRepoPattern = /^https:\/\/gitmcp\.io\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
5
|
+
|
|
6
|
+
export function isValidGitMcpEndpoint(url) {
|
|
7
|
+
return gitMcpRepoPattern.test(url);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function loadMcpServers() {
|
|
11
|
+
const raw = await fs.readFile(getMcpServersPath(), "utf8");
|
|
12
|
+
const parsed = JSON.parse(raw);
|
|
13
|
+
|
|
14
|
+
if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.servers)) {
|
|
15
|
+
throw new Error("mcp/servers.json must be an object with a servers array");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return parsed;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function validateMcpServers() {
|
|
22
|
+
const parsed = await loadMcpServers();
|
|
23
|
+
for (const server of parsed.servers) {
|
|
24
|
+
if (!isValidGitMcpEndpoint(server.endpoint)) {
|
|
25
|
+
throw new Error(`Invalid GitMCP endpoint: ${server.endpoint}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return parsed;
|
|
30
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
export class McpSseClient {
|
|
2
|
+
constructor(baseUrl) {
|
|
3
|
+
this.baseUrl = baseUrl;
|
|
4
|
+
this.controller = new AbortController();
|
|
5
|
+
this.reader = null;
|
|
6
|
+
this.buffer = "";
|
|
7
|
+
this.pending = new Map();
|
|
8
|
+
this.postUrl = null;
|
|
9
|
+
this.nextId = 1;
|
|
10
|
+
this.readLoopPromise = null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async connect() {
|
|
14
|
+
const response = await fetch(this.baseUrl, {
|
|
15
|
+
headers: { accept: "text/event-stream" },
|
|
16
|
+
signal: this.controller.signal,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
if (!response.ok || !response.body) {
|
|
20
|
+
throw new Error(`Failed to open MCP SSE stream: ${response.status}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
this.reader = response.body.getReader();
|
|
24
|
+
const endpointEvent = await this.#withTimeout(
|
|
25
|
+
this.#readNextEvent(),
|
|
26
|
+
5000,
|
|
27
|
+
"Timed out waiting for MCP endpoint event",
|
|
28
|
+
);
|
|
29
|
+
if (!endpointEvent || endpointEvent.event !== "endpoint") {
|
|
30
|
+
throw new Error("MCP server did not provide an endpoint event");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
this.postUrl = `${this.baseUrl}${endpointEvent.data}`;
|
|
34
|
+
this.readLoopPromise = this.#startReadLoop();
|
|
35
|
+
|
|
36
|
+
const initResult = await this.request("initialize", {
|
|
37
|
+
protocolVersion: "2025-03-26",
|
|
38
|
+
capabilities: {},
|
|
39
|
+
clientInfo: {
|
|
40
|
+
name: "meta-architect",
|
|
41
|
+
version: "0.1.0",
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
await this.notify("notifications/initialized");
|
|
46
|
+
return initResult;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async request(method, params = {}) {
|
|
50
|
+
const id = this.nextId++;
|
|
51
|
+
const responsePromise = new Promise((resolve, reject) => {
|
|
52
|
+
const timeout = setTimeout(() => {
|
|
53
|
+
this.pending.delete(id);
|
|
54
|
+
reject(new Error(`Timed out waiting for MCP response to ${method}`));
|
|
55
|
+
}, 15000);
|
|
56
|
+
|
|
57
|
+
this.pending.set(id, {
|
|
58
|
+
resolve: (payload) => {
|
|
59
|
+
clearTimeout(timeout);
|
|
60
|
+
resolve(payload);
|
|
61
|
+
},
|
|
62
|
+
reject: (error) => {
|
|
63
|
+
clearTimeout(timeout);
|
|
64
|
+
reject(error);
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
await this.#post({
|
|
70
|
+
jsonrpc: "2.0",
|
|
71
|
+
id,
|
|
72
|
+
method,
|
|
73
|
+
params,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return responsePromise;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async notify(method, params = {}) {
|
|
80
|
+
await this.#post({
|
|
81
|
+
jsonrpc: "2.0",
|
|
82
|
+
method,
|
|
83
|
+
params,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async close() {
|
|
88
|
+
this.controller.abort();
|
|
89
|
+
if (this.readLoopPromise) {
|
|
90
|
+
try {
|
|
91
|
+
await this.readLoopPromise;
|
|
92
|
+
} catch {
|
|
93
|
+
// Ignore abort-related shutdown failures.
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async #post(message) {
|
|
99
|
+
const response = await fetch(this.postUrl, {
|
|
100
|
+
method: "POST",
|
|
101
|
+
headers: { "content-type": "application/json" },
|
|
102
|
+
body: JSON.stringify(message),
|
|
103
|
+
signal: this.controller.signal,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
if (!response.ok && response.status !== 202) {
|
|
107
|
+
throw new Error(`MCP POST failed: ${response.status}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async #startReadLoop() {
|
|
112
|
+
try {
|
|
113
|
+
while (true) {
|
|
114
|
+
const event = await this.#readNextEvent();
|
|
115
|
+
if (!event) {
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (event.event !== "message") {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const payload = JSON.parse(event.data);
|
|
124
|
+
if (typeof payload.id === "number" && this.pending.has(payload.id)) {
|
|
125
|
+
const handlers = this.pending.get(payload.id);
|
|
126
|
+
this.pending.delete(payload.id);
|
|
127
|
+
if (payload.error) {
|
|
128
|
+
handlers.reject(new Error(payload.error.message ?? "Unknown MCP error"));
|
|
129
|
+
} else {
|
|
130
|
+
handlers.resolve(payload.result);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
} finally {
|
|
135
|
+
for (const [, handlers] of this.pending) {
|
|
136
|
+
handlers.reject(new Error("MCP stream closed"));
|
|
137
|
+
}
|
|
138
|
+
this.pending.clear();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async #readNextEvent() {
|
|
143
|
+
while (true) {
|
|
144
|
+
const result = await this.reader.read();
|
|
145
|
+
if (result.done) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
this.buffer += Buffer.from(result.value).toString("utf8");
|
|
150
|
+
const index = this.buffer.indexOf("\n\n");
|
|
151
|
+
if (index === -1) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const rawEvent = this.buffer.slice(0, index);
|
|
156
|
+
this.buffer = this.buffer.slice(index + 2);
|
|
157
|
+
return parseSseEvent(rawEvent);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async #withTimeout(promise, timeoutMs, message) {
|
|
162
|
+
return Promise.race([
|
|
163
|
+
promise,
|
|
164
|
+
new Promise((_, reject) => {
|
|
165
|
+
const timeout = setTimeout(() => {
|
|
166
|
+
clearTimeout(timeout);
|
|
167
|
+
reject(new Error(message));
|
|
168
|
+
}, timeoutMs);
|
|
169
|
+
}),
|
|
170
|
+
]);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function parseSseEvent(rawEvent) {
|
|
175
|
+
const event = { event: "message", data: "" };
|
|
176
|
+
for (const line of rawEvent.split("\n")) {
|
|
177
|
+
if (line.startsWith("event:")) {
|
|
178
|
+
event.event = line.slice("event:".length).trim();
|
|
179
|
+
} else if (line.startsWith("data:")) {
|
|
180
|
+
event.data += `${line.slice("data:".length).trim()}\n`;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
event.data = event.data.trimEnd();
|
|
185
|
+
return event;
|
|
186
|
+
}
|
package/src/paths.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
|
|
4
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
5
|
+
const __dirname = path.dirname(__filename);
|
|
6
|
+
|
|
7
|
+
export const packageRoot = path.resolve(__dirname, "..");
|
|
8
|
+
export function getRepoRoot() {
|
|
9
|
+
return process.env.MA_ROOT ? path.resolve(process.env.MA_ROOT) : process.cwd();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function getRuntimeRoot() {
|
|
13
|
+
return path.join(getRepoRoot(), ".ma");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function getRuntimeWritePath(...parts) {
|
|
17
|
+
return path.join(getRuntimeRoot(), ...parts);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getRuntimeReadPath(...parts) {
|
|
21
|
+
return path.join(getRuntimeRoot(), ...parts);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getMcpServersPath() {
|
|
25
|
+
return path.join(getRepoRoot(), "mcp", "servers.json");
|
|
26
|
+
}
|
package/src/policy.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function validateReleaseOrigin(originBranch) {
|
|
2
|
+
if (originBranch === "development") {
|
|
3
|
+
return true;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
if (originBranch.startsWith("release/")) {
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function rejectsDirectProdPromotion(originBranch) {
|
|
14
|
+
return originBranch.startsWith("feature/");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function validateMergeTarget(sourceBranch, targetBranch) {
|
|
18
|
+
return sourceBranch.startsWith("feature/") && targetBranch === "development";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function canMarkBuildDone(releaseState) {
|
|
22
|
+
return ["READY", "RUNNING"].includes(releaseState.build_status);
|
|
23
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import { writeJson } from "./fs-utils.js";
|
|
3
|
+
import { getRuntimeReadPath, getRuntimeWritePath } from "./paths.js";
|
|
4
|
+
|
|
5
|
+
const allowedStatuses = {
|
|
6
|
+
idea_status: ["DRAFT", "CLEAR", "BLOCKED"],
|
|
7
|
+
architecture_status: ["DRAFT", "REVIEWED", "APPROVED"],
|
|
8
|
+
evidence_status: ["MISSING", "PARTIAL", "VERIFIED"],
|
|
9
|
+
logic_status: ["PENDING", "GREEN", "RED"],
|
|
10
|
+
security_status: ["PENDING", "GREEN", "RED"],
|
|
11
|
+
experience_status: ["PENDING", "GREEN", "RED", "WAIVED"],
|
|
12
|
+
build_status: ["LOCKED", "READY", "RUNNING", "DONE"],
|
|
13
|
+
merge_status: ["LOCKED", "READY", "MERGED_TO_DEVELOPMENT"],
|
|
14
|
+
release_status: ["LOCKED", "READY", "SHIPPED_TO_PROD"],
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function validateWaiver(state) {
|
|
18
|
+
if (state.experience_status !== "WAIVED") {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const waiver = state.waiver;
|
|
23
|
+
if (!waiver?.reason || !waiver.actor || !waiver.timestamp) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
"experience_status=WAIVED requires waiver.reason, waiver.actor, and waiver.timestamp",
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function validateReleaseState(state) {
|
|
31
|
+
if (!state || typeof state !== "object" || Array.isArray(state)) {
|
|
32
|
+
throw new Error("Release state must be an object");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
for (const [field, allowed] of Object.entries(allowedStatuses)) {
|
|
36
|
+
if (!allowed.includes(state[field])) {
|
|
37
|
+
throw new Error(`Invalid ${field}: ${state[field]}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
validateWaiver(state);
|
|
42
|
+
return state;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function loadReleaseState() {
|
|
46
|
+
const raw = await fs.readFile(getRuntimeReadPath("release.json"), "utf8");
|
|
47
|
+
const parsed = JSON.parse(raw);
|
|
48
|
+
return validateReleaseState(parsed);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function saveReleaseState(state) {
|
|
52
|
+
validateReleaseState(state);
|
|
53
|
+
const next = {
|
|
54
|
+
...state,
|
|
55
|
+
updatedAt: new Date().toISOString(),
|
|
56
|
+
};
|
|
57
|
+
await writeJson(getRuntimeWritePath("release.json"), next);
|
|
58
|
+
return next;
|
|
59
|
+
}
|