@jstn-sdk/ma 0.1.13 → 0.14.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 +0 -0
- package/.codex/agents/Architect.toml +1 -0
- package/.codex/agents/Auditor.toml +1 -0
- package/.codex/agents/Builder.toml +1 -0
- package/.codex/agents/Flow.toml +1 -0
- package/.codex/agents/Sage.toml +1 -0
- package/.codex/agents/Vibe.toml +1 -0
- package/.codex/hooks.json +12 -2
- package/.codex/prompts/enforcement.md +0 -0
- package/.codex/prompts/onboarding.md +6 -1
- package/.codex/prompts/release-rules.md +0 -0
- package/.codex/prompts/skill-contract.md +0 -0
- package/COVERAGE.md +7 -7
- package/DEMO.md +15 -6
- package/LICENSE +0 -0
- package/README.md +126 -793
- package/bin/ma.js +585 -97
- package/data/clone-data.ledger.json +0 -0
- package/data/clone-data.proof.json +0 -0
- package/data/clone-data.rvf +0 -0
- package/docs/README.md +8 -2
- package/docs/autonomous-tasks.md +46 -0
- package/docs/codex-integration.md +16 -0
- package/docs/getting-started.md +28 -4
- package/docs/installed-sdk.md +0 -0
- package/docs/mcp-setup.md +30 -1
- package/docs/qa/{release-issue-gates-0.1.13.json → release-issue-gates-0.14.0.json} +77 -77
- package/docs/qa/release-readiness-0.1.5.md +0 -0
- package/docs/qa/{release-readiness-0.1.13.md → release-readiness-0.14.0.md} +12 -12
- package/docs/quality.md +24 -0
- package/docs/reference/native-engineering-patterns.md +0 -0
- package/docs/reference/native-security-playbooks.md +0 -0
- package/docs/reference/native-source-selection.md +0 -0
- package/docs/reference/native-style-and-deslop.md +0 -0
- package/docs/release-spec.md +27 -14
- package/docs/skills.md +1 -1
- package/index.js +73 -0
- package/mcp/collections.json +0 -0
- package/mcp/fallback.json +0 -0
- package/mcp/local/code-intel.js +179 -41
- package/mcp/local/context.js +180 -0
- package/mcp/local/memory.js +17 -6
- package/mcp/local/playbooks.js +0 -0
- package/mcp/local/quality.js +39 -0
- package/mcp/local/state.js +7 -3
- package/mcp/local/team-run.js +8 -3
- package/mcp/local/trace.js +0 -0
- package/mcp/local-capabilities.json +9 -0
- package/mcp/native-playbooks.json +0 -0
- package/mcp/servers.json +0 -0
- package/package.json +26 -4
- package/plugins/meta-architect/.app.json +1 -1
- package/plugins/meta-architect/.codex-plugin/plugin.json +1 -1
- package/plugins/meta-architect/.mcp.json +1 -1
- package/plugins/meta-architect/README.md +2 -2
- package/plugins/meta-architect/obsidian/main.js +137 -4
- package/plugins/meta-architect/obsidian/manifest.json +1 -1
- package/plugins/meta-architect/obsidian/styles.css +0 -0
- package/plugins/meta-architect/skills/align/SKILL.md +0 -0
- package/plugins/meta-architect/skills/align/agents/openai.yaml +0 -0
- package/plugins/meta-architect/skills/align/references/shared-language.md +0 -0
- package/plugins/meta-architect/skills/arch/SKILL.md +0 -0
- package/plugins/meta-architect/skills/arch/agents/openai.yaml +0 -0
- package/plugins/meta-architect/skills/build/SKILL.md +0 -0
- package/plugins/meta-architect/skills/build/agents/openai.yaml +0 -0
- package/plugins/meta-architect/skills/cleanup/SKILL.md +0 -0
- package/plugins/meta-architect/skills/cleanup/agents/openai.yaml +0 -0
- package/plugins/meta-architect/skills/cleanup/references/style-and-deslop.md +0 -0
- package/plugins/meta-architect/skills/diagnose/SKILL.md +0 -0
- package/plugins/meta-architect/skills/diagnose/agents/openai.yaml +0 -0
- package/plugins/meta-architect/skills/flow/SKILL.md +0 -0
- package/plugins/meta-architect/skills/flow/agents/openai.yaml +0 -0
- package/plugins/meta-architect/skills/maestro/SKILL.md +0 -0
- package/plugins/meta-architect/skills/maestro/agents/openai.yaml +0 -0
- package/plugins/meta-architect/skills/maestro/references/core-release-rules.md +0 -0
- package/plugins/meta-architect/skills/maestro/references/native-ingest-map.md +0 -0
- package/plugins/meta-architect/skills/sage/SKILL.md +0 -0
- package/plugins/meta-architect/skills/sage/agents/openai.yaml +0 -0
- package/plugins/meta-architect/skills/sage/references/source-selection.md +0 -0
- package/plugins/meta-architect/skills/tdd/SKILL.md +0 -0
- package/plugins/meta-architect/skills/tdd/agents/openai.yaml +0 -0
- package/plugins/meta-architect/skills/vet/SKILL.md +0 -0
- package/plugins/meta-architect/skills/vet/agents/openai.yaml +0 -0
- package/plugins/meta-architect/skills/vet/references/security-playbooks.md +0 -0
- package/plugins/meta-architect/skills/vibe/SKILL.md +0 -0
- package/plugins/meta-architect/skills/vibe/agents/openai.yaml +0 -0
- package/schemas/autonomous-task-queue.schema.json +56 -0
- package/schemas/handoff-packet.schema.json +36 -0
- package/schemas/setup-receipt.schema.json +28 -0
- package/schemas/skill-frontmatter.schema.json +20 -0
- package/schemas/support-bundle.schema.json +37 -0
- package/schemas/task-contract.schema.json +31 -0
- package/scripts/active-autonomy-hook.mjs +141 -11
- package/scripts/biome-staged.js +0 -0
- package/scripts/build-linux-packages.mjs +7 -3
- package/scripts/cleanup-test-fixtures.sh +55 -0
- package/scripts/context-hydration-hook.mjs +89 -0
- package/scripts/demo-smoke.js +285 -0
- package/scripts/doctor.js +10 -8
- package/scripts/install.sh +33 -4
- package/scripts/install.sh.sha256 +1 -0
- package/scripts/linux-package-lib.mjs +0 -0
- package/scripts/linux-package-smoke.mjs +2 -2
- package/scripts/package-size-check.mjs +26 -0
- package/scripts/plugin-sync.js +83 -17
- package/scripts/postinstall.js +17 -10
- package/scripts/prepack.js +9 -0
- package/scripts/ralph/prompt.md +0 -0
- package/scripts/release-metadata.js +0 -0
- package/scripts/release-sync.js +72 -7
- package/scripts/release-verify.js +100 -7
- package/scripts/setup-npmrc.js +48 -23
- package/scripts/skills-install.js +9 -2
- package/scripts/skills-manifest.js +2 -21
- package/scripts/skills-pack.js +2 -2
- package/scripts/skills-validate.js +5 -55
- package/skills/align/SKILL.md +0 -0
- package/skills/align/agents/openai.yaml +0 -0
- package/skills/align/references/shared-language.md +0 -0
- package/skills/arch/SKILL.md +0 -0
- package/skills/arch/agents/openai.yaml +0 -0
- package/skills/build/SKILL.md +0 -0
- package/skills/build/agents/openai.yaml +0 -0
- package/skills/cleanup/SKILL.md +0 -0
- package/skills/cleanup/agents/openai.yaml +0 -0
- package/skills/cleanup/references/style-and-deslop.md +0 -0
- package/skills/diagnose/SKILL.md +0 -0
- package/skills/diagnose/agents/openai.yaml +0 -0
- package/skills/flow/SKILL.md +0 -0
- package/skills/flow/agents/openai.yaml +0 -0
- package/skills/index.json +0 -0
- package/skills/maestro/SKILL.md +0 -0
- package/skills/maestro/agents/openai.yaml +0 -0
- package/skills/maestro/references/core-release-rules.md +0 -0
- package/skills/maestro/references/native-ingest-map.md +0 -0
- package/skills/sage/SKILL.md +0 -0
- package/skills/sage/agents/openai.yaml +0 -0
- package/skills/sage/references/source-selection.md +0 -0
- package/skills/tdd/SKILL.md +0 -0
- package/skills/tdd/agents/openai.yaml +0 -0
- package/skills/vet/SKILL.md +0 -0
- package/skills/vet/agents/openai.yaml +0 -0
- package/skills/vet/references/security-playbooks.md +0 -0
- package/skills/vibe/SKILL.md +0 -0
- package/skills/vibe/agents/openai.yaml +0 -0
- package/sprint/00-idea.md +0 -0
- package/sprint/01-architecture.md +0 -0
- package/sprint/02-oss-evidence.md +0 -0
- package/sprint/03-logic.md +0 -0
- package/sprint/04-security.md +0 -0
- package/sprint/05-dx-ux.md +0 -0
- package/sprint/06-build-plan.md +0 -0
- package/sprint/07-release.md +0 -0
- package/src/agents.js +279 -0
- package/src/bootstrap.js +252 -63
- package/src/build-gate.js +0 -0
- package/src/codex-app-server.js +291 -0
- package/src/decision-log.js +0 -0
- package/src/fs-utils.js +175 -4
- package/src/launcher.js +24 -21
- package/src/mcp-config.js +9 -1
- package/src/mcp-live-client.js +140 -7
- package/src/paths.js +6 -1
- package/src/policy.js +2 -2
- package/src/prelaunch.js +189 -0
- package/src/quality/ai-quality-orchestrator.js +328 -0
- package/src/release-issue-gates.js +62 -0
- package/src/release-operations.js +62 -0
- package/src/release-state.js +0 -0
- package/src/runtime/active-autonomy-core.js +0 -0
- package/src/runtime/agent-compat.js +31 -0
- package/src/runtime/alignment-sentinel.js +0 -0
- package/src/runtime/architect-review.js +248 -10
- package/src/runtime/autonomous-tasks.js +408 -0
- package/src/runtime/build-readiness.js +0 -0
- package/src/runtime/code-graph-rehearse.js +0 -0
- package/src/runtime/codeburn-core.js +112 -0
- package/src/runtime/context-authority.js +40 -0
- package/src/runtime/context-economy-core.js +40 -0
- package/src/runtime/continuity-graph.js +201 -0
- package/src/runtime/continuity-notes.js +71 -26
- package/src/runtime/core-source-ingest.js +0 -0
- package/src/runtime/detached-provider.js +82 -14
- package/src/runtime/doctor-report.js +18 -0
- package/src/runtime/environment-awareness-core.js +0 -0
- package/src/runtime/exposure-catalog.js +0 -0
- package/src/runtime/graphify-core.js +167 -0
- package/src/runtime/guidance-stack.js +9 -2
- package/src/runtime/handoff-packets.js +92 -0
- package/src/runtime/headroom-core.js +104 -0
- package/src/runtime/helper-orchestration-core.js +0 -0
- package/src/runtime/hook-profiles.js +20 -0
- package/src/runtime/learning-loop-core.js +110 -4
- package/src/runtime/live-agent-verification.js +122 -0
- package/src/runtime/maestro-events.js +0 -0
- package/src/runtime/maestro-manager.js +0 -0
- package/src/runtime/maestro-output.js +58 -0
- package/src/runtime/maestro-state.js +0 -0
- package/src/runtime/managed-markdown.js +29 -0
- package/src/runtime/mcp-authority.js +147 -0
- package/src/runtime/mcp-policy.js +0 -0
- package/src/runtime/obsidian-integration-core.js +255 -17
- package/src/runtime/obsidian-plugin-bridge.js +344 -38
- package/src/runtime/orchestrator.js +0 -0
- package/src/runtime/pi-maestro-core.js +89 -0
- package/src/runtime/preferences.js +79 -0
- package/src/runtime/project-context.js +636 -0
- package/src/runtime/prompt-strategy-core.js +0 -0
- package/src/runtime/quorum-review.js +0 -0
- package/src/runtime/ralph-execution-core.js +0 -0
- package/src/runtime/redaction-gateway.js +218 -19
- package/src/runtime/runtime-state.js +94 -2
- package/src/runtime/schema-migrations.js +162 -0
- package/src/runtime/semantic-recording-core.js +0 -0
- package/src/runtime/signal-hooks.js +13 -0
- package/src/runtime/skills-registry-export.js +92 -8
- package/src/runtime/startup-path.js +0 -0
- package/src/runtime/task-contracts.js +83 -0
- package/src/runtime/universal-plugin-broker-core.js +373 -50
- package/src/runtime/workspace-intelligence-runtime.js +0 -0
- package/src/runtime/workspace-virtualizer.js +0 -0
- package/src/runtime/workspaces.js +0 -0
- package/src/runtime-artifacts.js +0 -0
- package/src/setup-lifecycle.js +359 -0
- package/src/skill-frontmatter.js +78 -0
- package/src/skill-installer.js +226 -71
- package/src/skills.js +297 -19
- package/src/state-sync.js +0 -0
- package/src/test-fixtures.js +543 -0
- package/src/tui/grid.js +67 -0
- package/src/tui/status-grid.js +23 -0
- package/support-bundle.json +114 -0
- package/templates/AGENTS.md +6 -2
- package/templates/catalog-manifest.json +0 -0
- package/templates/model-instructions/core.md +0 -0
- package/templates/model-instructions/release.md +0 -0
- package/templates/model-instructions/security.md +0 -0
- package/templates/quality/ai-quality-rules.yml +37 -0
- package/docs/assets/image/Screenshot(1).png +0 -0
- package/docs/assets/image/Screenshot(2).png +0 -0
- package/docs/assets/image/Screenshot(3).png +0 -0
- package/docs/assets/image/Screenshot(4).png +0 -0
- package/docs/assets/image/Screenshot(5).png +0 -0
- package/docs/assets/image/Screenshot(6).png +0 -0
- package/docs/assets/image/Screenshot(7).png +0 -0
- package/docs/assets/image/Screenshot(8).png +0 -0
- package/docs/assets/image/Screenshot(9).png +0 -0
- package/docs/assets/meta-architect-logo.png +0 -0
- package/docs/assets/meta-architect-logo.svg +0 -8
- package/docs/onboarding.md +0 -81
- package/docs/prompt-guidance-contract.md +0 -17
- package/docs/prompt-guidance-fragments/active-autonomy-core.md +0 -7
- package/docs/skills-publishing.md +0 -255
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
2
3
|
import fs from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
3
5
|
import { readJson, writeFileIfMissing, writeJson } from "../fs-utils.js";
|
|
4
6
|
import { getRuntimeStatePath } from "../paths.js";
|
|
5
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
maskSensitiveText,
|
|
9
|
+
redactProviderBoundPayload,
|
|
10
|
+
redactProviderBoundText,
|
|
11
|
+
seedRedactionVault,
|
|
12
|
+
} from "./redaction-gateway.js";
|
|
13
|
+
|
|
14
|
+
const REVIEW_POLICY_PATH = path.join(".ma", "architect-review-policy.json");
|
|
15
|
+
const DEFAULT_REVIEW_TIMEOUT_MS = 120000;
|
|
16
|
+
const MAX_REVIEW_OUTPUT_BYTES = 1024 * 1024;
|
|
6
17
|
|
|
7
18
|
export function getArchitectReviewPath() {
|
|
8
19
|
return getRuntimeStatePath("architect-review.json");
|
|
@@ -57,32 +68,259 @@ export async function runExternalArchitectReview({ prompt = "" } = {}) {
|
|
|
57
68
|
const outputPath = getRuntimeStatePath("architect-review.output.json");
|
|
58
69
|
await fs.writeFile(promptPath, `${redactedPrompt.sanitizedText.trimEnd()}\n`, "utf8");
|
|
59
70
|
|
|
60
|
-
const
|
|
71
|
+
const { command: binary, args } = parseTrustedCommand(
|
|
72
|
+
command,
|
|
73
|
+
redactedPrompt.sanitizedText,
|
|
74
|
+
getArchitectReviewConfiguration(),
|
|
75
|
+
);
|
|
76
|
+
const timeout = readReviewTimeoutMs();
|
|
77
|
+
const result = spawnSync(binary, args, {
|
|
61
78
|
cwd: process.cwd(),
|
|
62
|
-
env: {
|
|
63
|
-
...process.env,
|
|
79
|
+
env: buildArchitectReviewEnv({
|
|
64
80
|
MA_ARCHITECT_REVIEW_PROMPT: redactedPrompt.sanitizedText,
|
|
65
81
|
MA_ARCHITECT_REVIEW_PROMPT_FILE: promptPath,
|
|
66
82
|
MA_ARCHITECT_REVIEW_OUTPUT: outputPath,
|
|
67
|
-
},
|
|
83
|
+
}),
|
|
68
84
|
encoding: "utf8",
|
|
85
|
+
shell: false,
|
|
86
|
+
timeout,
|
|
87
|
+
maxBuffer: MAX_REVIEW_OUTPUT_BYTES,
|
|
88
|
+
killSignal: "SIGKILL",
|
|
69
89
|
});
|
|
70
90
|
|
|
91
|
+
if (result.error) {
|
|
92
|
+
await writeReviewReceipt({
|
|
93
|
+
status: "failed",
|
|
94
|
+
command: binary,
|
|
95
|
+
timeout,
|
|
96
|
+
error: result.error.message,
|
|
97
|
+
});
|
|
98
|
+
throw new Error(`External architect review failed: ${result.error.message}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
71
101
|
if (result.status !== 0) {
|
|
102
|
+
await writeReviewReceipt({
|
|
103
|
+
status: "failed",
|
|
104
|
+
command: binary,
|
|
105
|
+
timeout,
|
|
106
|
+
code: result.status,
|
|
107
|
+
stderr: result.stderr,
|
|
108
|
+
});
|
|
72
109
|
throw new Error(
|
|
73
|
-
|
|
110
|
+
maskSensitiveText(
|
|
111
|
+
result.stderr?.trim() || result.stdout?.trim() || "External architect review failed",
|
|
112
|
+
).sanitizedText,
|
|
74
113
|
);
|
|
75
114
|
}
|
|
76
115
|
|
|
116
|
+
let outputStat;
|
|
117
|
+
try {
|
|
118
|
+
outputStat = await fs.stat(outputPath);
|
|
119
|
+
} catch {
|
|
120
|
+
await writeReviewReceipt({
|
|
121
|
+
status: "failed",
|
|
122
|
+
command: binary,
|
|
123
|
+
timeout,
|
|
124
|
+
error: "reviewer did not produce the configured JSON output",
|
|
125
|
+
});
|
|
126
|
+
throw new Error("External architect review did not produce JSON output");
|
|
127
|
+
}
|
|
128
|
+
if (outputStat.size > MAX_REVIEW_OUTPUT_BYTES) {
|
|
129
|
+
await writeReviewReceipt({
|
|
130
|
+
status: "failed",
|
|
131
|
+
command: binary,
|
|
132
|
+
timeout,
|
|
133
|
+
error: `review output exceeds ${MAX_REVIEW_OUTPUT_BYTES} bytes`,
|
|
134
|
+
});
|
|
135
|
+
throw new Error(`External architect review output exceeds ${MAX_REVIEW_OUTPUT_BYTES} bytes`);
|
|
136
|
+
}
|
|
77
137
|
const parsed = JSON.parse(await fs.readFile(outputPath, "utf8"));
|
|
138
|
+
const safeOutput = await redactProviderBoundPayload(parsed, {
|
|
139
|
+
kind: "architect-review-output",
|
|
140
|
+
});
|
|
141
|
+
const sanitized = safeOutput.sanitizedPayload;
|
|
78
142
|
const review = {
|
|
79
143
|
schemaVersion: "0.1.0",
|
|
80
|
-
verdict:
|
|
81
|
-
reviewer:
|
|
82
|
-
summary:
|
|
83
|
-
findings: Array.isArray(
|
|
144
|
+
verdict: sanitized.verdict ?? "UNKNOWN",
|
|
145
|
+
reviewer: sanitized.reviewer ?? "external",
|
|
146
|
+
summary: sanitized.summary ?? null,
|
|
147
|
+
findings: Array.isArray(sanitized.findings) ? sanitized.findings : [],
|
|
84
148
|
reviewedAt: new Date().toISOString(),
|
|
85
149
|
};
|
|
86
150
|
await saveArchitectReview(review);
|
|
151
|
+
await writeReviewReceipt({
|
|
152
|
+
status: "completed",
|
|
153
|
+
command: binary,
|
|
154
|
+
timeout,
|
|
155
|
+
verdict: review.verdict,
|
|
156
|
+
});
|
|
87
157
|
return review;
|
|
88
158
|
}
|
|
159
|
+
|
|
160
|
+
export function getArchitectReviewConfiguration(root = process.env.MA_ROOT ?? process.cwd()) {
|
|
161
|
+
const allowedCommands = new Set(
|
|
162
|
+
(process.env.MA_ARCHITECT_REVIEW_ALLOWLIST ?? "")
|
|
163
|
+
.split(",")
|
|
164
|
+
.map((value) => value.trim())
|
|
165
|
+
.filter(Boolean),
|
|
166
|
+
);
|
|
167
|
+
const policyPath = path.join(root, REVIEW_POLICY_PATH);
|
|
168
|
+
try {
|
|
169
|
+
const policy = JSON.parse(readFileSync(policyPath, "utf8"));
|
|
170
|
+
for (const value of policy.allowedCommands ?? []) {
|
|
171
|
+
if (typeof value === "string" && value.trim()) allowedCommands.add(value.trim());
|
|
172
|
+
}
|
|
173
|
+
return { allowedCommands: [...allowedCommands], policyPath, source: "environment+project" };
|
|
174
|
+
} catch {
|
|
175
|
+
return {
|
|
176
|
+
allowedCommands: [...allowedCommands],
|
|
177
|
+
policyPath,
|
|
178
|
+
source: allowedCommands.size > 0 ? "environment" : "none",
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function parseTrustedCommand(command, promptText, configuration = {}) {
|
|
184
|
+
if (/[;&|`<>]/.test(command)) {
|
|
185
|
+
throw new Error("MA_ARCHITECT_REVIEW_CMD must not contain shell metacharacters");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const parts = splitCommand(command);
|
|
189
|
+
if (parts.length === 0) {
|
|
190
|
+
throw new Error("MA_ARCHITECT_REVIEW_CMD is empty");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (parts[0].includes("=") && !parts[0].includes("/")) {
|
|
194
|
+
throw new Error("MA_ARCHITECT_REVIEW_CMD must start with an executable");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const substituted = parts.map((part) => part.replaceAll("{prompt}", promptText));
|
|
198
|
+
const configured = new Set(configuration.allowedCommands ?? []);
|
|
199
|
+
const commandName = path.basename(substituted[0]);
|
|
200
|
+
if (!configured.has(substituted[0]) && !configured.has(commandName)) {
|
|
201
|
+
throw new Error(
|
|
202
|
+
`MA_ARCHITECT_REVIEW_CMD is not allowlisted: ${substituted[0]}. ` +
|
|
203
|
+
"Add its exact command or basename to MA_ARCHITECT_REVIEW_ALLOWLIST or .ma/architect-review-policy.json.",
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
command: substituted[0],
|
|
208
|
+
args: substituted.slice(1),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function readReviewTimeoutMs() {
|
|
213
|
+
const parsed = Number(process.env.MA_ARCHITECT_REVIEW_TIMEOUT_MS);
|
|
214
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_REVIEW_TIMEOUT_MS;
|
|
215
|
+
return Math.min(120000, Math.max(1000, Math.floor(parsed)));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function writeReviewReceipt(details) {
|
|
219
|
+
const receiptDir = path.join(
|
|
220
|
+
process.env.MA_ROOT ?? process.cwd(),
|
|
221
|
+
".ma",
|
|
222
|
+
"evidence",
|
|
223
|
+
"architect-review-receipts",
|
|
224
|
+
);
|
|
225
|
+
const safeDetails = { ...details };
|
|
226
|
+
for (const key of ["error", "stderr"]) {
|
|
227
|
+
if (safeDetails[key])
|
|
228
|
+
safeDetails[key] = maskSensitiveText(String(safeDetails[key])).sanitizedText;
|
|
229
|
+
}
|
|
230
|
+
await fs.mkdir(receiptDir, { recursive: true }).catch(() => {});
|
|
231
|
+
await fs
|
|
232
|
+
.writeFile(
|
|
233
|
+
path.join(receiptDir, `review-${Date.now()}-${process.pid}.json`),
|
|
234
|
+
`${JSON.stringify(
|
|
235
|
+
{
|
|
236
|
+
schemaVersion: "0.1.0",
|
|
237
|
+
record_type: "architect_review_receipt",
|
|
238
|
+
generatedAt: new Date().toISOString(),
|
|
239
|
+
...safeDetails,
|
|
240
|
+
},
|
|
241
|
+
null,
|
|
242
|
+
2,
|
|
243
|
+
)}\n`,
|
|
244
|
+
{ mode: 0o600 },
|
|
245
|
+
)
|
|
246
|
+
.catch(() => {});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function buildArchitectReviewEnv(extraEnv = {}) {
|
|
250
|
+
const passthroughKeys = new Set([
|
|
251
|
+
"PATH",
|
|
252
|
+
"HOME",
|
|
253
|
+
"USERPROFILE",
|
|
254
|
+
"TMPDIR",
|
|
255
|
+
"TMP",
|
|
256
|
+
"TEMP",
|
|
257
|
+
"LANG",
|
|
258
|
+
"LC_ALL",
|
|
259
|
+
"SYSTEMROOT",
|
|
260
|
+
"COMSPEC",
|
|
261
|
+
"MA_ROOT",
|
|
262
|
+
]);
|
|
263
|
+
const env = {};
|
|
264
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
265
|
+
if (passthroughKeys.has(key)) {
|
|
266
|
+
env[key] = value;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return { ...env, ...extraEnv };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function splitCommand(command) {
|
|
273
|
+
const parts = [];
|
|
274
|
+
let current = "";
|
|
275
|
+
let quote = null;
|
|
276
|
+
let escaping = false;
|
|
277
|
+
|
|
278
|
+
for (const char of command) {
|
|
279
|
+
if (escaping) {
|
|
280
|
+
current += char;
|
|
281
|
+
escaping = false;
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (char === "\\") {
|
|
286
|
+
escaping = true;
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (quote) {
|
|
291
|
+
if (char === quote) {
|
|
292
|
+
quote = null;
|
|
293
|
+
} else {
|
|
294
|
+
current += char;
|
|
295
|
+
}
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (char === "'" || char === '"') {
|
|
300
|
+
quote = char;
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (/\s/.test(char)) {
|
|
305
|
+
if (current) {
|
|
306
|
+
parts.push(current);
|
|
307
|
+
current = "";
|
|
308
|
+
}
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
current += char;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (escaping) {
|
|
316
|
+
current += "\\";
|
|
317
|
+
}
|
|
318
|
+
if (quote) {
|
|
319
|
+
throw new Error("MA_ARCHITECT_REVIEW_CMD has an unterminated quote");
|
|
320
|
+
}
|
|
321
|
+
if (current) {
|
|
322
|
+
parts.push(current);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return parts;
|
|
326
|
+
}
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { getAgentInvocation } from "../agents.js";
|
|
5
|
+
import { readJson, withRuntimeStateLock, writeFileIfMissing, writeJson } from "../fs-utils.js";
|
|
6
|
+
import { getRuntimeSubsystemPath } from "../paths.js";
|
|
7
|
+
import { writeJsonAtomically } from "../setup-lifecycle.js";
|
|
8
|
+
import {
|
|
9
|
+
createDefaultEnvironmentAwarenessCore,
|
|
10
|
+
discoverEnvironmentCapabilities,
|
|
11
|
+
selectEnvironmentCapabilitiesForTask,
|
|
12
|
+
} from "./environment-awareness-core.js";
|
|
13
|
+
import { appendMaestroEvent } from "./maestro-events.js";
|
|
14
|
+
import { createTaskContract, validateTaskContract } from "./task-contracts.js";
|
|
15
|
+
|
|
16
|
+
export const autonomousTaskSchemaVersion = "0.1.0";
|
|
17
|
+
const statuses = new Set(["queued", "running", "completed", "failed", "blocked", "cancelled"]);
|
|
18
|
+
const priorities = new Set(["low", "normal", "high", "critical"]);
|
|
19
|
+
|
|
20
|
+
export function getAutonomousTaskQueuePath() {
|
|
21
|
+
return getRuntimeSubsystemPath("tasks", "autonomous-queue.json");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function createTaskQueue() {
|
|
25
|
+
return {
|
|
26
|
+
schemaVersion: autonomousTaskSchemaVersion,
|
|
27
|
+
record_type: "autonomous_task_queue",
|
|
28
|
+
tasks: [],
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function seedAutonomousTaskArtifacts() {
|
|
33
|
+
const queuePath = getAutonomousTaskQueuePath();
|
|
34
|
+
await fs.mkdir(path.dirname(queuePath), { recursive: true });
|
|
35
|
+
await writeFileIfMissing(queuePath, `${JSON.stringify(createTaskQueue(), null, 2)}\n`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizeTaskId(value) {
|
|
39
|
+
const id = String(value ?? "")
|
|
40
|
+
.trim()
|
|
41
|
+
.replace(/[^a-zA-Z0-9._-]/g, "-");
|
|
42
|
+
if (!id || id === "." || id === "..") throw new Error("task id is required");
|
|
43
|
+
return id;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function normalizeTask(input) {
|
|
47
|
+
const goal = String(input?.goal ?? input?.title ?? "").trim();
|
|
48
|
+
if (!goal) throw new Error("autonomous task requires goal");
|
|
49
|
+
const id = normalizeTaskId(input?.id ?? `task-${randomUUID().slice(0, 8)}`);
|
|
50
|
+
const priority = input?.priority ?? "normal";
|
|
51
|
+
if (!priorities.has(priority)) throw new Error(`unsupported task priority: ${priority}`);
|
|
52
|
+
const dependencies = Array.isArray(input?.dependencies ?? input?.dependsOn)
|
|
53
|
+
? [...(input.dependencies ?? input.dependsOn)].map(normalizeTaskId)
|
|
54
|
+
: [];
|
|
55
|
+
const labels = Array.isArray(input?.labels) ? [...new Set(input.labels.map(String))] : [];
|
|
56
|
+
const deadline = input?.deadline ?? null;
|
|
57
|
+
if (deadline && Number.isNaN(Date.parse(deadline)))
|
|
58
|
+
throw new Error("task deadline must be an ISO date");
|
|
59
|
+
const contract = input?.contract
|
|
60
|
+
? validateTaskContract(input.contract)
|
|
61
|
+
: createTaskContract({
|
|
62
|
+
goal,
|
|
63
|
+
contextUsed: [
|
|
64
|
+
".ma/context/project-index.json",
|
|
65
|
+
".ma/context/environment-awareness-core.json",
|
|
66
|
+
],
|
|
67
|
+
constraints: ["Stop for destructive, credential-gated, or external production actions"],
|
|
68
|
+
verification: ["ma doctor", "Maestro lane receipts"],
|
|
69
|
+
stopCondition: "Complete with verification evidence or record a blocker",
|
|
70
|
+
risk: input?.risk ?? "medium",
|
|
71
|
+
});
|
|
72
|
+
return {
|
|
73
|
+
id,
|
|
74
|
+
goal,
|
|
75
|
+
priority,
|
|
76
|
+
dependencies,
|
|
77
|
+
labels,
|
|
78
|
+
deadline,
|
|
79
|
+
status: input?.status ?? "queued",
|
|
80
|
+
attempts: Number.isInteger(input?.attempts) ? input.attempts : 0,
|
|
81
|
+
maxAttempts: Number.isInteger(input?.maxAttempts) ? Math.max(1, input.maxAttempts) : 3,
|
|
82
|
+
vendor: input?.vendor ?? null,
|
|
83
|
+
contract,
|
|
84
|
+
createdAt: input?.createdAt ?? new Date().toISOString(),
|
|
85
|
+
updatedAt: new Date().toISOString(),
|
|
86
|
+
startedAt: input?.startedAt ?? null,
|
|
87
|
+
completedAt: input?.completedAt ?? null,
|
|
88
|
+
error: input?.error ?? null,
|
|
89
|
+
blocker: input?.blocker ?? null,
|
|
90
|
+
evidence: Array.isArray(input?.evidence) ? [...input.evidence] : [],
|
|
91
|
+
selectedCapabilities: Array.isArray(input?.selectedCapabilities)
|
|
92
|
+
? input.selectedCapabilities
|
|
93
|
+
: [],
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function validateTaskQueue(queue) {
|
|
98
|
+
if (
|
|
99
|
+
!queue ||
|
|
100
|
+
queue.schemaVersion !== autonomousTaskSchemaVersion ||
|
|
101
|
+
queue.record_type !== "autonomous_task_queue"
|
|
102
|
+
)
|
|
103
|
+
throw new Error("invalid autonomous task queue schema");
|
|
104
|
+
if (!Array.isArray(queue.tasks)) throw new Error("autonomous task queue requires tasks");
|
|
105
|
+
const ids = new Set();
|
|
106
|
+
for (const task of queue.tasks) {
|
|
107
|
+
if (ids.has(task.id)) throw new Error(`duplicate autonomous task id: ${task.id}`);
|
|
108
|
+
ids.add(task.id);
|
|
109
|
+
if (
|
|
110
|
+
!statuses.has(task.status) ||
|
|
111
|
+
!priorities.has(task.priority) ||
|
|
112
|
+
!Array.isArray(task.dependencies)
|
|
113
|
+
)
|
|
114
|
+
throw new Error(`invalid task state: ${task.id}`);
|
|
115
|
+
validateTaskContract(task.contract);
|
|
116
|
+
if (task.dependencies.some((dependency) => dependency === task.id))
|
|
117
|
+
throw new Error(`task cannot depend on itself: ${task.id}`);
|
|
118
|
+
}
|
|
119
|
+
for (const task of queue.tasks)
|
|
120
|
+
for (const dependency of task.dependencies)
|
|
121
|
+
if (!ids.has(dependency)) throw new Error(`unknown task dependency: ${dependency}`);
|
|
122
|
+
for (const task of queue.tasks) {
|
|
123
|
+
const visiting = new Set();
|
|
124
|
+
const visit = (id) => {
|
|
125
|
+
if (visiting.has(id)) throw new Error(`cyclic task dependency: ${id}`);
|
|
126
|
+
visiting.add(id);
|
|
127
|
+
for (const dependency of byId(queue.tasks).get(id)?.dependencies ?? []) visit(dependency);
|
|
128
|
+
visiting.delete(id);
|
|
129
|
+
};
|
|
130
|
+
visit(task.id);
|
|
131
|
+
}
|
|
132
|
+
return queue;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function byId(tasks) {
|
|
136
|
+
return new Map(tasks.map((task) => [task.id, task]));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function loadTaskQueueUnlocked() {
|
|
140
|
+
try {
|
|
141
|
+
const queue = validateTaskQueue(await readJson(getAutonomousTaskQueuePath()));
|
|
142
|
+
const interrupted = queue.tasks.filter((task) => task.status === "running");
|
|
143
|
+
if (interrupted.length === 0) return queue;
|
|
144
|
+
const recoveredAt = new Date().toISOString();
|
|
145
|
+
for (const task of interrupted) {
|
|
146
|
+
task.status = "queued";
|
|
147
|
+
task.error = "Recovered after the previous process exited while the task was running";
|
|
148
|
+
task.updatedAt = recoveredAt;
|
|
149
|
+
}
|
|
150
|
+
await writeJsonAtomically(getAutonomousTaskQueuePath(), queue);
|
|
151
|
+
return queue;
|
|
152
|
+
} catch (error) {
|
|
153
|
+
if (error?.code === "ENOENT") return createTaskQueue();
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export async function loadTaskQueue() {
|
|
159
|
+
return withRuntimeStateLock(getAutonomousTaskQueuePath(), loadTaskQueueUnlocked, { wait: true });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export async function saveTaskQueue(queue) {
|
|
163
|
+
await fs.mkdir(path.dirname(getAutonomousTaskQueuePath()), { recursive: true });
|
|
164
|
+
return writeJson(getAutonomousTaskQueuePath(), validateTaskQueue(queue));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export async function enqueueAutonomousTasks(inputs) {
|
|
168
|
+
return withRuntimeStateLock(
|
|
169
|
+
getAutonomousTaskQueuePath(),
|
|
170
|
+
async () => {
|
|
171
|
+
const queue = await loadTaskQueueUnlocked();
|
|
172
|
+
const tasks = Array.isArray(inputs) ? inputs : [inputs];
|
|
173
|
+
const added = tasks.map(normalizeTask);
|
|
174
|
+
const existing = new Set(queue.tasks.map((task) => task.id));
|
|
175
|
+
for (const task of added) {
|
|
176
|
+
if (existing.has(task.id)) throw new Error(`task already exists: ${task.id}`);
|
|
177
|
+
existing.add(task.id);
|
|
178
|
+
queue.tasks.push(task);
|
|
179
|
+
}
|
|
180
|
+
await writeJsonAtomically(getAutonomousTaskQueuePath(), validateTaskQueue(queue));
|
|
181
|
+
return added;
|
|
182
|
+
},
|
|
183
|
+
{ wait: true },
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export async function cancelAutonomousTask(id, reason = "Cancelled by operator") {
|
|
188
|
+
return withRuntimeStateLock(
|
|
189
|
+
getAutonomousTaskQueuePath(),
|
|
190
|
+
async () => {
|
|
191
|
+
const queue = await loadTaskQueueUnlocked();
|
|
192
|
+
const task = queue.tasks.find((entry) => entry.id === normalizeTaskId(id));
|
|
193
|
+
if (!task) throw new Error(`unknown autonomous task: ${id}`);
|
|
194
|
+
if (["completed", "cancelled"].includes(task.status)) return task;
|
|
195
|
+
task.status = "cancelled";
|
|
196
|
+
task.blocker = reason;
|
|
197
|
+
task.updatedAt = new Date().toISOString();
|
|
198
|
+
await writeJsonAtomically(getAutonomousTaskQueuePath(), validateTaskQueue(queue));
|
|
199
|
+
return task;
|
|
200
|
+
},
|
|
201
|
+
{ wait: true },
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function hasUnsafeAction(task) {
|
|
206
|
+
return /\b(deploy|production|publish|delete\s+all|drop\s+(database|table)|rotate\s+credentials|send\s+money)\b/i.test(
|
|
207
|
+
task.goal,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function dependenciesReady(task, byId) {
|
|
212
|
+
return task.dependencies.every((id) => byId.get(id)?.status === "completed");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function isExpired(task, now) {
|
|
216
|
+
return task.deadline && Date.parse(task.deadline) < now;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export async function runAutonomousTasks({
|
|
220
|
+
concurrency = 1,
|
|
221
|
+
maxTasks = Infinity,
|
|
222
|
+
includeGlobal = true,
|
|
223
|
+
execute = null,
|
|
224
|
+
cwd = process.cwd(),
|
|
225
|
+
onEvent = null,
|
|
226
|
+
} = {}) {
|
|
227
|
+
if (!Number.isInteger(concurrency) || concurrency < 1)
|
|
228
|
+
throw new Error("concurrency must be a positive integer");
|
|
229
|
+
const queue = await loadTaskQueue();
|
|
230
|
+
const byId = new Map(queue.tasks.map((task) => [task.id, task]));
|
|
231
|
+
const capabilities = createDefaultEnvironmentAwarenessCore({
|
|
232
|
+
capabilities: await discoverEnvironmentCapabilities({ cwd, includeGlobal }),
|
|
233
|
+
});
|
|
234
|
+
const runner =
|
|
235
|
+
execute ??
|
|
236
|
+
(async (task) => {
|
|
237
|
+
const { runMaestro } = await import("../skills.js");
|
|
238
|
+
await runMaestro({ taskContract: task.contract, handoff: { nextAction: task.goal } });
|
|
239
|
+
const [{ loadManagerRunRegistryOrDefault }, { loadAlignmentSentinelReport }] =
|
|
240
|
+
await Promise.all([import("./maestro-manager.js"), import("./alignment-sentinel.js")]);
|
|
241
|
+
const [managerRegistry, alignment] = await Promise.all([
|
|
242
|
+
loadManagerRunRegistryOrDefault(),
|
|
243
|
+
loadAlignmentSentinelReport(),
|
|
244
|
+
]);
|
|
245
|
+
const latestRun = managerRegistry.runs.at(-1);
|
|
246
|
+
if (alignment?.driftStatus === "DRIFTED") {
|
|
247
|
+
return {
|
|
248
|
+
status: "blocked",
|
|
249
|
+
reason: "Maestro blocked dispatch because alignment drift must be resolved",
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
if (latestRun && ["waiting-review", "blocked"].includes(latestRun.state)) {
|
|
253
|
+
return {
|
|
254
|
+
status: "blocked",
|
|
255
|
+
reason: `Maestro ${latestRun.state}: ${latestRun.retry?.lastReason ?? "review or blocker remains"}`,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
return { status: "completed", evidence: ["Maestro manager run completed"] };
|
|
259
|
+
});
|
|
260
|
+
const events = [];
|
|
261
|
+
const emit = async (event) => {
|
|
262
|
+
events.push(event);
|
|
263
|
+
await appendMaestroEvent({ record_type: "autonomous_task", ...event });
|
|
264
|
+
await onEvent?.(event);
|
|
265
|
+
};
|
|
266
|
+
const running = new Set();
|
|
267
|
+
let processed = 0;
|
|
268
|
+
while (processed + running.size < maxTasks) {
|
|
269
|
+
const now = Date.now();
|
|
270
|
+
const eligible = queue.tasks
|
|
271
|
+
.filter((task) => task.status === "queued" && !running.has(task.id))
|
|
272
|
+
.filter((task) => !isExpired(task, now) && dependenciesReady(task, byId))
|
|
273
|
+
.sort(
|
|
274
|
+
(a, b) =>
|
|
275
|
+
(priorities.has(b.priority)
|
|
276
|
+
? ["low", "normal", "high", "critical"].indexOf(b.priority)
|
|
277
|
+
: 0) - ["low", "normal", "high", "critical"].indexOf(a.priority),
|
|
278
|
+
);
|
|
279
|
+
if (eligible.length === 0 || running.size >= concurrency) {
|
|
280
|
+
if (running.size === 0) break;
|
|
281
|
+
await Promise.race(running);
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
const remaining = maxTasks - processed - running.size;
|
|
285
|
+
for (const task of eligible.slice(0, Math.min(concurrency - running.size, remaining))) {
|
|
286
|
+
task.selectedCapabilities = selectEnvironmentCapabilitiesForTask(
|
|
287
|
+
capabilities,
|
|
288
|
+
task.goal,
|
|
289
|
+
).selected;
|
|
290
|
+
task.invocation = task.vendor ? getAgentInvocation(task.vendor, "maestro") : null;
|
|
291
|
+
if (hasUnsafeAction(task)) {
|
|
292
|
+
task.status = "blocked";
|
|
293
|
+
task.blocker =
|
|
294
|
+
"Explicit approval required for destructive, production, credential, or external mutation task";
|
|
295
|
+
task.updatedAt = new Date().toISOString();
|
|
296
|
+
await emit({ taskId: task.id, status: task.status, blocker: task.blocker });
|
|
297
|
+
processed++;
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
task.status = "running";
|
|
301
|
+
task.attempts++;
|
|
302
|
+
task.startedAt ??= new Date().toISOString();
|
|
303
|
+
task.updatedAt = new Date().toISOString();
|
|
304
|
+
const promise = (async () => {
|
|
305
|
+
try {
|
|
306
|
+
const result = await runner(task);
|
|
307
|
+
if (result?.status === "blocked") {
|
|
308
|
+
task.status = "blocked";
|
|
309
|
+
task.blocker = result.reason ?? "Runner reported a blocker";
|
|
310
|
+
} else if (result?.status === "failed" && task.attempts < task.maxAttempts) {
|
|
311
|
+
task.status = "queued";
|
|
312
|
+
task.error = result.reason ?? "Runner failed; task queued for retry";
|
|
313
|
+
} else if (result?.status === "failed") {
|
|
314
|
+
task.status = "failed";
|
|
315
|
+
task.error = result.reason ?? "Runner failed after maximum attempts";
|
|
316
|
+
} else {
|
|
317
|
+
task.status = "completed";
|
|
318
|
+
task.completedAt = new Date().toISOString();
|
|
319
|
+
task.evidence.push(...(Array.isArray(result?.evidence) ? result.evidence : []));
|
|
320
|
+
}
|
|
321
|
+
} catch (error) {
|
|
322
|
+
task.error = error instanceof Error ? error.message : String(error);
|
|
323
|
+
task.status = task.attempts < task.maxAttempts ? "queued" : "failed";
|
|
324
|
+
}
|
|
325
|
+
task.updatedAt = new Date().toISOString();
|
|
326
|
+
await emit({
|
|
327
|
+
taskId: task.id,
|
|
328
|
+
status: task.status,
|
|
329
|
+
attempts: task.attempts,
|
|
330
|
+
error: task.error,
|
|
331
|
+
blocker: task.blocker,
|
|
332
|
+
});
|
|
333
|
+
await saveTaskQueue(queue);
|
|
334
|
+
running.delete(promise);
|
|
335
|
+
processed++;
|
|
336
|
+
})();
|
|
337
|
+
running.add(promise);
|
|
338
|
+
await saveTaskQueue(queue);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
await Promise.all(running);
|
|
342
|
+
for (const task of queue.tasks) {
|
|
343
|
+
if (task.status !== "queued") continue;
|
|
344
|
+
const failedDependency = task.dependencies.find((id) =>
|
|
345
|
+
["failed", "blocked", "cancelled"].includes(byId.get(id)?.status),
|
|
346
|
+
);
|
|
347
|
+
if (failedDependency) {
|
|
348
|
+
task.status = "blocked";
|
|
349
|
+
task.blocker = `Dependency ${failedDependency} is ${byId.get(failedDependency).status}`;
|
|
350
|
+
} else if (isExpired(task, Date.now())) {
|
|
351
|
+
task.status = "blocked";
|
|
352
|
+
task.blocker = "Task deadline expired";
|
|
353
|
+
} else if (!dependenciesReady(task, byId)) {
|
|
354
|
+
task.blocker = "Waiting for dependencies";
|
|
355
|
+
}
|
|
356
|
+
task.updatedAt = new Date().toISOString();
|
|
357
|
+
}
|
|
358
|
+
await saveTaskQueue(queue);
|
|
359
|
+
return {
|
|
360
|
+
schemaVersion: autonomousTaskSchemaVersion,
|
|
361
|
+
tasks: queue.tasks,
|
|
362
|
+
events,
|
|
363
|
+
summary: summarizeTasks(queue.tasks),
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export function summarizeTasks(tasks) {
|
|
368
|
+
return Object.fromEntries(
|
|
369
|
+
[...statuses].map((status) => [status, tasks.filter((task) => task.status === status).length]),
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export async function parseTaskInput(raw, format = "json") {
|
|
374
|
+
if (format === "json" || format === "jsonl") return JSON.parse(raw);
|
|
375
|
+
if (format !== "yaml" && format !== "yml")
|
|
376
|
+
throw new Error(`unsupported task input format: ${format}`);
|
|
377
|
+
const result = [];
|
|
378
|
+
let current = null;
|
|
379
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
380
|
+
const trimmed = line.trim();
|
|
381
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
382
|
+
if (trimmed === "tasks:") continue;
|
|
383
|
+
if (trimmed.startsWith("- ")) {
|
|
384
|
+
if (current) result.push(current);
|
|
385
|
+
current = {};
|
|
386
|
+
parseYamlField(current, trimmed.slice(2));
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
if (!current) current = {};
|
|
390
|
+
parseYamlField(current, trimmed);
|
|
391
|
+
}
|
|
392
|
+
if (current) result.push(current);
|
|
393
|
+
return result.length === 1 ? result[0] : result;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function parseYamlField(target, line) {
|
|
397
|
+
const index = line.indexOf(":");
|
|
398
|
+
if (index < 1) throw new Error(`invalid task YAML line: ${line}`);
|
|
399
|
+
const key = line.slice(0, index).trim();
|
|
400
|
+
const value = line.slice(index + 1).trim();
|
|
401
|
+
if (value.startsWith("[") && value.endsWith("]"))
|
|
402
|
+
target[key] = value
|
|
403
|
+
.slice(1, -1)
|
|
404
|
+
.split(",")
|
|
405
|
+
.map((item) => item.trim())
|
|
406
|
+
.filter(Boolean);
|
|
407
|
+
else target[key] = value.replace(/^['"]|['"]$/g, "");
|
|
408
|
+
}
|
|
File without changes
|
|
File without changes
|