@aipper/aiws 0.0.24 → 0.0.26
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 +89 -33
- package/package.json +3 -3
- package/src/cli.js +40 -2
- package/src/codex-skills.js +4 -4
- package/src/commands/change-advice.js +191 -0
- package/src/commands/change-evidence-command.js +242 -0
- package/src/commands/change-evidence-entry.js +40 -0
- package/src/commands/change-evidence.js +338 -0
- package/src/commands/change-finish.js +484 -0
- package/src/commands/change-git-status.js +53 -0
- package/src/commands/change-lifecycle-entry.js +60 -0
- package/src/commands/change-lifecycle.js +234 -0
- package/src/commands/change-metrics-command.js +110 -0
- package/src/commands/change-next-command.js +132 -0
- package/src/commands/change-review-gates.js +315 -0
- package/src/commands/change-scope-gate.js +34 -0
- package/src/commands/change-start.js +138 -0
- package/src/commands/change-state-command.js +40 -0
- package/src/commands/change-status-command.js +126 -0
- package/src/commands/change-status-context.js +202 -0
- package/src/commands/change-validate-entry.js +45 -0
- package/src/commands/change-validate.js +127 -0
- package/src/commands/change.js +540 -1692
- package/src/commands/dashboard.js +38 -12
- package/src/commands/init.js +1 -1
- package/src/commands/opencode-status.js +61 -0
- package/src/commands/update.js +5 -1
- package/src/dashboard/app.js +205 -2
- package/src/dashboard/index.html +5 -2
- package/src/dashboard/style.css +86 -0
- package/src/governance.js +157 -0
- package/src/json-schema-lite.js +164 -0
- package/src/opencode-env.js +76 -0
- package/src/spec.js +131 -0
- package/src/template.js +42 -2
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { UserError } from "./errors.js";
|
|
2
|
+
import { loadWorkflowGovernanceRules } from "./spec.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @param {any} status
|
|
6
|
+
*/
|
|
7
|
+
export function effectiveReviewCount(status) {
|
|
8
|
+
return Number(status?.reviewSignals?.effectiveCount ?? status?.collaboration?.counts?.review ?? 0);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {any} status
|
|
13
|
+
* @param {any} [governance]
|
|
14
|
+
*/
|
|
15
|
+
function collectGovernanceSignals(status, governance) {
|
|
16
|
+
const git = status?.git && typeof status.git === "object" ? status.git : {};
|
|
17
|
+
const lifecycle = status?.lifecycle && typeof status.lifecycle === "object" ? status.lifecycle : {};
|
|
18
|
+
const repo = status?.repo && typeof status.repo === "object" ? status.repo : {};
|
|
19
|
+
const gov = governance && typeof governance === "object" ? governance : status?.governance && typeof status.governance === "object" ? status.governance : {};
|
|
20
|
+
const reviewGates = status?.reviewGates && typeof status.reviewGates === "object" ? status.reviewGates : {};
|
|
21
|
+
const reviewGateSummary = reviewGates?.summary && typeof reviewGates.summary === "object" ? reviewGates.summary : {};
|
|
22
|
+
const reviewEffectiveCount = effectiveReviewCount(status);
|
|
23
|
+
const specReviewReady = reviewGates?.specReview?.ready === true || (!reviewGates.specReview && reviewEffectiveCount > 0);
|
|
24
|
+
const qualityReviewReady = reviewGates?.qualityReview?.ready === true || (!reviewGates.qualityReview && reviewEffectiveCount > 0);
|
|
25
|
+
const verifyBeforeCompleteReady = reviewGates?.verifyBeforeComplete?.ready === true;
|
|
26
|
+
const validateStampReady = reviewGates?.validateStamp?.ready === true;
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
blockers_strict_count: Array.isArray(status?.blockersStrict) ? status.blockersStrict.length : 0,
|
|
30
|
+
tasks_unchecked: Number(status?.tasks?.unchecked || 0),
|
|
31
|
+
review_effective_count: reviewEffectiveCount,
|
|
32
|
+
evidence_persistent_count: Number(status?.evidence?.counts?.persistent || 0),
|
|
33
|
+
git_total: Number(git.total || 0),
|
|
34
|
+
git_staged: Number(git.staged || 0),
|
|
35
|
+
git_conflicted: Number(git.conflicted || 0),
|
|
36
|
+
lifecycle_evidence_runs: Number(lifecycle.evidenceRuns || 0),
|
|
37
|
+
finish_cleanup_pending_reason: String(lifecycle.finishCleanupPendingReason || ""),
|
|
38
|
+
finish_state: String(lifecycle.finishState || ""),
|
|
39
|
+
finish_state_reason: String(lifecycle.finishStateReason || ""),
|
|
40
|
+
repo_submodules: Number(repo.submodules || 0),
|
|
41
|
+
spec_review_ready: specReviewReady,
|
|
42
|
+
quality_review_ready: qualityReviewReady,
|
|
43
|
+
verify_before_complete_ready: verifyBeforeCompleteReady,
|
|
44
|
+
validate_stamp_ready: validateStampReady,
|
|
45
|
+
spec_review_status: String(reviewGates?.specReview?.status || (specReviewReady ? "ready" : "missing")),
|
|
46
|
+
quality_review_status: String(reviewGates?.qualityReview?.status || (qualityReviewReady ? "ready" : "missing")),
|
|
47
|
+
verify_before_complete_status: String(
|
|
48
|
+
reviewGates?.verifyBeforeComplete?.status || (verifyBeforeCompleteReady ? "ready" : "missing"),
|
|
49
|
+
),
|
|
50
|
+
validate_stamp_status: String(reviewGates?.validateStamp?.status || (validateStampReady ? "ready" : "missing")),
|
|
51
|
+
dual_review_missing_count: Number(reviewGateSummary?.dualReviewMissingCount || 0),
|
|
52
|
+
finish_gate_missing_count: Number(reviewGateSummary?.finishGateMissingCount || 0),
|
|
53
|
+
governance_current_stage: String(gov.currentStage || ""),
|
|
54
|
+
governance_recommended_stage: String(gov.recommendedStage || ""),
|
|
55
|
+
governance_rule_id: String(gov.ruleId || ""),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {string} template
|
|
61
|
+
* @param {Record<string, string | number | boolean>} signals
|
|
62
|
+
*/
|
|
63
|
+
function renderTemplate(template, signals) {
|
|
64
|
+
return String(template || "").replace(/\{([a-z0-9_]+)\}/gi, (_, key) => String(signals[key] ?? ""));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @param {unknown} actual
|
|
69
|
+
* @param {any} condition
|
|
70
|
+
*/
|
|
71
|
+
function matchesLeafCondition(actual, condition) {
|
|
72
|
+
if (Object.prototype.hasOwnProperty.call(condition, "eq") && actual !== condition.eq) return false;
|
|
73
|
+
if (Object.prototype.hasOwnProperty.call(condition, "ne") && actual === condition.ne) return false;
|
|
74
|
+
if (Object.prototype.hasOwnProperty.call(condition, "gt") && !(Number(actual) > Number(condition.gt))) return false;
|
|
75
|
+
if (Object.prototype.hasOwnProperty.call(condition, "gte") && !(Number(actual) >= Number(condition.gte))) return false;
|
|
76
|
+
if (Object.prototype.hasOwnProperty.call(condition, "lt") && !(Number(actual) < Number(condition.lt))) return false;
|
|
77
|
+
if (Object.prototype.hasOwnProperty.call(condition, "lte") && !(Number(actual) <= Number(condition.lte))) return false;
|
|
78
|
+
if (Array.isArray(condition?.in) && !condition.in.includes(actual)) return false;
|
|
79
|
+
if (Array.isArray(condition?.notIn) && condition.notIn.includes(actual)) return false;
|
|
80
|
+
if (condition?.truthy === true && !actual) return false;
|
|
81
|
+
if (condition?.falsy === true && !!actual) return false;
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @param {any} when
|
|
87
|
+
* @param {Record<string, string | number | boolean>} signals
|
|
88
|
+
*/
|
|
89
|
+
function matchesCondition(when, signals) {
|
|
90
|
+
if (!when || (typeof when === "object" && Object.keys(when).length === 0)) return true;
|
|
91
|
+
if (Array.isArray(when?.all)) return when.all.every((item) => matchesCondition(item, signals));
|
|
92
|
+
if (Array.isArray(when?.any)) return when.any.some((item) => matchesCondition(item, signals));
|
|
93
|
+
if (when?.not) return !matchesCondition(when.not, signals);
|
|
94
|
+
if (typeof when?.signal === "string") {
|
|
95
|
+
return matchesLeafCondition(signals[when.signal], when);
|
|
96
|
+
}
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* @param {string[]} lines
|
|
102
|
+
*/
|
|
103
|
+
function uniqueLines(lines) {
|
|
104
|
+
const seen = new Set();
|
|
105
|
+
/** @type {string[]} */
|
|
106
|
+
const out = [];
|
|
107
|
+
for (const raw of lines || []) {
|
|
108
|
+
const line = String(raw || "").trim();
|
|
109
|
+
if (!line || seen.has(line)) continue;
|
|
110
|
+
seen.add(line);
|
|
111
|
+
out.push(line);
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* @param {any} status
|
|
118
|
+
*/
|
|
119
|
+
export async function inferChangeGovernance(status) {
|
|
120
|
+
const spec = await loadWorkflowGovernanceRules();
|
|
121
|
+
const signals = collectGovernanceSignals(status);
|
|
122
|
+
const matched = spec.governanceRules.find((rule) => matchesCondition(rule?.when, signals));
|
|
123
|
+
if (!matched) {
|
|
124
|
+
throw new UserError("Failed to infer workflow governance from spec rules.", {
|
|
125
|
+
details: JSON.stringify({ source: spec.source, signals }, null, 2),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
phase: String(status?.phase || ""),
|
|
130
|
+
currentStage: String(matched.currentStage || ""),
|
|
131
|
+
recommendedStage: String(matched.recommendedStage || matched.currentStage || ""),
|
|
132
|
+
rationale: renderTemplate(String(matched.rationale || ""), signals),
|
|
133
|
+
ruleId: String(matched.id || ""),
|
|
134
|
+
source: spec.source,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* @param {any} status
|
|
140
|
+
*/
|
|
141
|
+
export async function governanceGuidanceLines(status) {
|
|
142
|
+
const spec = await loadWorkflowGovernanceRules();
|
|
143
|
+
const governance = status?.governance && typeof status.governance === "object" ? status.governance : await inferChangeGovernance(status);
|
|
144
|
+
const signals = collectGovernanceSignals(status, governance);
|
|
145
|
+
/** @type {string[]} */
|
|
146
|
+
const lines = [];
|
|
147
|
+
|
|
148
|
+
for (const rule of spec.guidanceRules) {
|
|
149
|
+
if (!matchesCondition(rule?.when, signals)) continue;
|
|
150
|
+
const entries = Array.isArray(rule?.lines) ? rule.lines : [];
|
|
151
|
+
for (const entry of entries) lines.push(renderTemplate(String(entry || ""), signals));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const deduped = uniqueLines(lines);
|
|
155
|
+
if (deduped.length > 0) return deduped;
|
|
156
|
+
return ["按阶段契约继续推进当前 change"];
|
|
157
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { UserError } from "./errors.js";
|
|
2
|
+
|
|
3
|
+
function isPlainObject(value) {
|
|
4
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function joinPath(base, segment) {
|
|
8
|
+
if (!base || base === "$") return `$${segment}`;
|
|
9
|
+
return `${base}${segment}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function resolvePointer(rootSchema, ref) {
|
|
13
|
+
if (typeof ref !== "string" || !ref.startsWith("#/")) {
|
|
14
|
+
throw new UserError("Unsupported schema $ref.", { details: String(ref || "") });
|
|
15
|
+
}
|
|
16
|
+
const parts = ref
|
|
17
|
+
.slice(2)
|
|
18
|
+
.split("/")
|
|
19
|
+
.map((part) => part.replace(/~1/g, "/").replace(/~0/g, "~"));
|
|
20
|
+
let cur = rootSchema;
|
|
21
|
+
for (const part of parts) {
|
|
22
|
+
if (!isPlainObject(cur) && !Array.isArray(cur)) return undefined;
|
|
23
|
+
cur = cur[part];
|
|
24
|
+
}
|
|
25
|
+
return cur;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function typeMatches(value, type) {
|
|
29
|
+
switch (type) {
|
|
30
|
+
case "object":
|
|
31
|
+
return isPlainObject(value);
|
|
32
|
+
case "array":
|
|
33
|
+
return Array.isArray(value);
|
|
34
|
+
case "string":
|
|
35
|
+
return typeof value === "string";
|
|
36
|
+
case "number":
|
|
37
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
38
|
+
case "integer":
|
|
39
|
+
return typeof value === "number" && Number.isInteger(value);
|
|
40
|
+
case "boolean":
|
|
41
|
+
return typeof value === "boolean";
|
|
42
|
+
case "null":
|
|
43
|
+
return value === null;
|
|
44
|
+
default:
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param {any} value
|
|
51
|
+
* @param {any} schema
|
|
52
|
+
* @param {any} rootSchema
|
|
53
|
+
* @param {string} curPath
|
|
54
|
+
* @param {string[]} errors
|
|
55
|
+
*/
|
|
56
|
+
function validateNode(value, schema, rootSchema, curPath, errors) {
|
|
57
|
+
if (!isPlainObject(schema)) return;
|
|
58
|
+
|
|
59
|
+
if (schema.$ref) {
|
|
60
|
+
const target = resolvePointer(rootSchema, schema.$ref);
|
|
61
|
+
if (!target) {
|
|
62
|
+
errors.push(`${curPath}: unresolved $ref ${schema.$ref}`);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
validateNode(value, target, rootSchema, curPath, errors);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (Array.isArray(schema.anyOf) && schema.anyOf.length > 0) {
|
|
70
|
+
const matched = schema.anyOf.some((candidate) => {
|
|
71
|
+
const branchErrors = [];
|
|
72
|
+
validateNode(value, candidate, rootSchema, curPath, branchErrors);
|
|
73
|
+
return branchErrors.length === 0;
|
|
74
|
+
});
|
|
75
|
+
if (!matched) errors.push(`${curPath}: does not match anyOf`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (Array.isArray(schema.allOf) && schema.allOf.length > 0) {
|
|
79
|
+
for (const candidate of schema.allOf) validateNode(value, candidate, rootSchema, curPath, errors);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (Array.isArray(schema.oneOf) && schema.oneOf.length > 0) {
|
|
83
|
+
let matches = 0;
|
|
84
|
+
for (const candidate of schema.oneOf) {
|
|
85
|
+
const branchErrors = [];
|
|
86
|
+
validateNode(value, candidate, rootSchema, curPath, branchErrors);
|
|
87
|
+
if (branchErrors.length === 0) matches += 1;
|
|
88
|
+
}
|
|
89
|
+
if (matches !== 1) errors.push(`${curPath}: does not match exactly one schema`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (schema.type && !typeMatches(value, schema.type)) {
|
|
93
|
+
errors.push(`${curPath}: expected ${schema.type}`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (Object.prototype.hasOwnProperty.call(schema, "const") && value !== schema.const) {
|
|
98
|
+
errors.push(`${curPath}: expected const ${JSON.stringify(schema.const)}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (Array.isArray(schema.enum) && !schema.enum.includes(value)) {
|
|
102
|
+
errors.push(`${curPath}: expected one of ${schema.enum.map((item) => JSON.stringify(item)).join(", ")}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (typeof value === "string") {
|
|
106
|
+
if (typeof schema.minLength === "number" && value.length < schema.minLength) {
|
|
107
|
+
errors.push(`${curPath}: expected minLength ${schema.minLength}`);
|
|
108
|
+
}
|
|
109
|
+
if (typeof schema.pattern === "string") {
|
|
110
|
+
const re = new RegExp(schema.pattern);
|
|
111
|
+
if (!re.test(value)) errors.push(`${curPath}: does not match pattern ${schema.pattern}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (Array.isArray(value)) {
|
|
116
|
+
if (typeof schema.minItems === "number" && value.length < schema.minItems) {
|
|
117
|
+
errors.push(`${curPath}: expected minItems ${schema.minItems}`);
|
|
118
|
+
}
|
|
119
|
+
if (schema.items) {
|
|
120
|
+
value.forEach((item, index) => validateNode(item, schema.items, rootSchema, joinPath(curPath, `[${index}]`), errors));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (isPlainObject(value)) {
|
|
125
|
+
const entries = Object.keys(value);
|
|
126
|
+
if (typeof schema.minProperties === "number" && entries.length < schema.minProperties) {
|
|
127
|
+
errors.push(`${curPath}: expected minProperties ${schema.minProperties}`);
|
|
128
|
+
}
|
|
129
|
+
if (typeof schema.maxProperties === "number" && entries.length > schema.maxProperties) {
|
|
130
|
+
errors.push(`${curPath}: expected maxProperties ${schema.maxProperties}`);
|
|
131
|
+
}
|
|
132
|
+
const properties = isPlainObject(schema.properties) ? schema.properties : {};
|
|
133
|
+
const required = Array.isArray(schema.required) ? schema.required : [];
|
|
134
|
+
for (const key of required) {
|
|
135
|
+
if (!Object.prototype.hasOwnProperty.call(value, key)) errors.push(`${curPath}: missing required property ${key}`);
|
|
136
|
+
}
|
|
137
|
+
for (const [key, childSchema] of Object.entries(properties)) {
|
|
138
|
+
if (!Object.prototype.hasOwnProperty.call(value, key)) continue;
|
|
139
|
+
validateNode(value[key], childSchema, rootSchema, joinPath(curPath, `.${key}`), errors);
|
|
140
|
+
}
|
|
141
|
+
if (schema.additionalProperties === false) {
|
|
142
|
+
for (const key of entries) {
|
|
143
|
+
if (!Object.prototype.hasOwnProperty.call(properties, key)) {
|
|
144
|
+
errors.push(`${curPath}: unexpected property ${key}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* @param {any} value
|
|
153
|
+
* @param {any} schema
|
|
154
|
+
* @param {{ source: string }} options
|
|
155
|
+
*/
|
|
156
|
+
export function validateJsonSchemaLite(value, schema, options) {
|
|
157
|
+
const errors = [];
|
|
158
|
+
validateNode(value, schema, schema, "$", errors);
|
|
159
|
+
if (errors.length > 0) {
|
|
160
|
+
throw new UserError("Spec doc schema validation failed.", {
|
|
161
|
+
details: [options?.source || "", ...errors].filter(Boolean).join("\n"),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { pathExists, readText } from "./fs.js";
|
|
3
|
+
|
|
4
|
+
const OPENCODE_AGENT_NAMES = ["planner-sisyphus", "librarian", "explore", "oracle"];
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {string} value
|
|
8
|
+
*/
|
|
9
|
+
function relativeOrDot(value) {
|
|
10
|
+
return value ? value.replaceAll(path.sep, "/") : ".";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {any} raw
|
|
15
|
+
* @param {string} agentName
|
|
16
|
+
*/
|
|
17
|
+
function readAgentState(raw, agentName) {
|
|
18
|
+
const agents = raw && typeof raw === "object" && raw.agents && typeof raw.agents === "object" ? raw.agents : {};
|
|
19
|
+
const entry = agents[agentName];
|
|
20
|
+
return {
|
|
21
|
+
present: !!entry && typeof entry === "object",
|
|
22
|
+
enabled: !!(entry && typeof entry === "object" && entry.enabled === true),
|
|
23
|
+
replacePlan: entry && typeof entry === "object" && typeof entry.replace_plan === "boolean" ? entry.replace_plan : null,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {string} workspaceRoot
|
|
29
|
+
*/
|
|
30
|
+
export async function detectOpenCodeEnvironment(workspaceRoot) {
|
|
31
|
+
const opencodeDir = path.join(workspaceRoot, ".opencode");
|
|
32
|
+
const configPath = path.join(opencodeDir, "oh-my-opencode.json");
|
|
33
|
+
const examplePath = path.join(opencodeDir, "oh-my-opencode.json.example");
|
|
34
|
+
const opencodeDirExists = await pathExists(opencodeDir);
|
|
35
|
+
const configExists = await pathExists(configPath);
|
|
36
|
+
const exampleExists = await pathExists(examplePath);
|
|
37
|
+
|
|
38
|
+
/** @type {any} */
|
|
39
|
+
let config = null;
|
|
40
|
+
let configStatus = "missing";
|
|
41
|
+
let configError = "";
|
|
42
|
+
if (configExists) {
|
|
43
|
+
try {
|
|
44
|
+
config = JSON.parse(await readText(configPath));
|
|
45
|
+
configStatus = "loaded";
|
|
46
|
+
} catch (error) {
|
|
47
|
+
configStatus = "invalid";
|
|
48
|
+
configError = error instanceof Error ? error.message : String(error);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const agents = Object.fromEntries(OPENCODE_AGENT_NAMES.map((name) => [name, readAgentState(config, name)]));
|
|
53
|
+
const enabledAgents = OPENCODE_AGENT_NAMES.filter((name) => agents[name]?.enabled === true);
|
|
54
|
+
const recommendedAgentsReady = ["planner-sisyphus", "librarian", "explore", "oracle"].every((name) => agents[name]?.enabled === true);
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
workspaceRoot,
|
|
58
|
+
opencodeDir,
|
|
59
|
+
opencodeDirExists,
|
|
60
|
+
configPath,
|
|
61
|
+
configExists,
|
|
62
|
+
configStatus,
|
|
63
|
+
configError,
|
|
64
|
+
examplePath,
|
|
65
|
+
exampleExists,
|
|
66
|
+
mode: configStatus === "loaded" ? "oMo-enabled" : "standard-opencode",
|
|
67
|
+
recommendedAgentsReady,
|
|
68
|
+
enabledAgents,
|
|
69
|
+
agents,
|
|
70
|
+
rel: {
|
|
71
|
+
opencodeDir: relativeOrDot(path.relative(workspaceRoot, opencodeDir)),
|
|
72
|
+
configPath: relativeOrDot(path.relative(workspaceRoot, configPath)),
|
|
73
|
+
examplePath: relativeOrDot(path.relative(workspaceRoot, examplePath)),
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
package/src/spec.js
CHANGED
|
@@ -4,6 +4,9 @@ import { createRequire } from "node:module";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { pathExists, readText } from "./fs.js";
|
|
6
6
|
import { UserError } from "./errors.js";
|
|
7
|
+
import { validateJsonSchemaLite } from "./json-schema-lite.js";
|
|
8
|
+
|
|
9
|
+
const specJsonDocCache = new Map();
|
|
7
10
|
|
|
8
11
|
/**
|
|
9
12
|
* @returns {Promise<{ rootDir: string, version: string }>}
|
|
@@ -62,3 +65,131 @@ export async function loadTemplate(templateId) {
|
|
|
62
65
|
const manifest = JSON.parse(await readText(manifestPath));
|
|
63
66
|
return { templateId, templateDir, manifest, specVersion: spec.version };
|
|
64
67
|
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @param {string} relPath
|
|
71
|
+
*/
|
|
72
|
+
async function loadSpecJsonDoc(relPath) {
|
|
73
|
+
const spec = await loadSpecPackage();
|
|
74
|
+
const absPath = path.join(spec.rootDir, relPath);
|
|
75
|
+
const cacheKey = `${spec.rootDir}:${relPath}`;
|
|
76
|
+
if (specJsonDocCache.has(cacheKey)) {
|
|
77
|
+
return { spec, data: specJsonDocCache.get(cacheKey), absPath };
|
|
78
|
+
}
|
|
79
|
+
const raw = await readText(absPath);
|
|
80
|
+
let data;
|
|
81
|
+
try {
|
|
82
|
+
data = JSON.parse(raw);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
throw new UserError(`Invalid JSON in spec doc: ${relPath}`, {
|
|
85
|
+
details: error instanceof Error ? error.message : String(error),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
specJsonDocCache.set(cacheKey, data);
|
|
89
|
+
return { spec, data, absPath };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function loadValidatedSpecJsonDoc(relPath, schemaRelPath) {
|
|
93
|
+
const loaded = await loadSpecJsonDoc(relPath);
|
|
94
|
+
const { data: schema } = await loadSpecJsonDoc(schemaRelPath);
|
|
95
|
+
validateJsonSchemaLite(loaded.data, schema, { source: loaded.absPath });
|
|
96
|
+
return loaded;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function loadWorkflowGovernanceRules() {
|
|
100
|
+
const relPath = path.join("docs", "workflow-governance-rules.json");
|
|
101
|
+
const schemaRelPath = path.join("docs", "workflow-governance-rules.schema.json");
|
|
102
|
+
const { spec, data } = await loadValidatedSpecJsonDoc(relPath, schemaRelPath);
|
|
103
|
+
const governanceRules = data.governanceRules;
|
|
104
|
+
const guidanceRules = data.guidanceRules;
|
|
105
|
+
return {
|
|
106
|
+
ok: true,
|
|
107
|
+
source: "@aipper/aiws-spec/docs/workflow-governance-rules.json",
|
|
108
|
+
specVersion: spec.version,
|
|
109
|
+
version: Number(data?.version || 1),
|
|
110
|
+
description: String(data?.description || ""),
|
|
111
|
+
governanceRules,
|
|
112
|
+
guidanceRules,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function loadWorkflowStageContracts() {
|
|
117
|
+
const relPath = path.join("docs", "workflow-stage-contracts.json");
|
|
118
|
+
const schemaRelPath = path.join("docs", "workflow-stage-contracts.schema.json");
|
|
119
|
+
const { spec, data } = await loadValidatedSpecJsonDoc(relPath, schemaRelPath);
|
|
120
|
+
const stages = data.stages;
|
|
121
|
+
return {
|
|
122
|
+
ok: true,
|
|
123
|
+
source: "@aipper/aiws-spec/docs/workflow-stage-contracts.json",
|
|
124
|
+
specVersion: spec.version,
|
|
125
|
+
version: Number(data?.version || 1),
|
|
126
|
+
description: String(data?.description || ""),
|
|
127
|
+
standardChain: Array.isArray(data?.standardChain) ? data.standardChain : [],
|
|
128
|
+
notes: Array.isArray(data?.notes) ? data.notes : [],
|
|
129
|
+
stages,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function loadWorkflowRouterRules() {
|
|
134
|
+
const relPath = path.join("docs", "workflow-router-rules.json");
|
|
135
|
+
const schemaRelPath = path.join("docs", "workflow-router-rules.schema.json");
|
|
136
|
+
const { spec, data } = await loadValidatedSpecJsonDoc(relPath, schemaRelPath);
|
|
137
|
+
return {
|
|
138
|
+
ok: true,
|
|
139
|
+
source: "@aipper/aiws-spec/docs/workflow-router-rules.json",
|
|
140
|
+
specVersion: spec.version,
|
|
141
|
+
version: Number(data?.version || 1),
|
|
142
|
+
title: String(data?.title || ""),
|
|
143
|
+
description: String(data?.description || ""),
|
|
144
|
+
truthFiles: Array.isArray(data?.truthFiles) ? data.truthFiles : [],
|
|
145
|
+
bootstrapEntry: data.bootstrapEntry,
|
|
146
|
+
universalSequence: Array.isArray(data?.universalSequence) ? data.universalSequence : [],
|
|
147
|
+
routingRules: Array.isArray(data?.routingRules) ? data.routingRules : [],
|
|
148
|
+
routeCases: Array.isArray(data?.routeCases) ? data.routeCases : [],
|
|
149
|
+
clarificationTriggers: Array.isArray(data?.clarificationTriggers) ? data.clarificationTriggers : [],
|
|
150
|
+
universalRules: Array.isArray(data?.universalRules) ? data.universalRules : [],
|
|
151
|
+
notes: Array.isArray(data?.notes) ? data.notes : [],
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export async function loadWorkflowReviewGates() {
|
|
156
|
+
const relPath = path.join("docs", "workflow-review-gates.json");
|
|
157
|
+
const schemaRelPath = path.join("docs", "workflow-review-gates.schema.json");
|
|
158
|
+
const { spec, data } = await loadValidatedSpecJsonDoc(relPath, schemaRelPath);
|
|
159
|
+
return {
|
|
160
|
+
ok: true,
|
|
161
|
+
source: "@aipper/aiws-spec/docs/workflow-review-gates.json",
|
|
162
|
+
specVersion: spec.version,
|
|
163
|
+
version: Number(data?.version || 1),
|
|
164
|
+
title: String(data?.title || ""),
|
|
165
|
+
description: String(data?.description || ""),
|
|
166
|
+
gates: Array.isArray(data?.gates) ? data.gates : [],
|
|
167
|
+
unifiedRules: Array.isArray(data?.unifiedRules) ? data.unifiedRules : [],
|
|
168
|
+
evidence: Array.isArray(data?.evidence) ? data.evidence : [],
|
|
169
|
+
notes: Array.isArray(data?.notes) ? data.notes : [],
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export async function loadWorkflowDelegationContracts() {
|
|
174
|
+
const relPath = path.join("docs", "workflow-delegation-contracts.json");
|
|
175
|
+
const schemaRelPath = path.join("docs", "workflow-delegation-contracts.schema.json");
|
|
176
|
+
const { spec, data } = await loadValidatedSpecJsonDoc(relPath, schemaRelPath);
|
|
177
|
+
return {
|
|
178
|
+
ok: true,
|
|
179
|
+
source: "@aipper/aiws-spec/docs/workflow-delegation-contracts.json",
|
|
180
|
+
specVersion: spec.version,
|
|
181
|
+
version: Number(data?.version || 1),
|
|
182
|
+
title: String(data?.title || ""),
|
|
183
|
+
description: String(data?.description || ""),
|
|
184
|
+
purpose: Array.isArray(data?.purpose) ? data.purpose : [],
|
|
185
|
+
truthFiles: Array.isArray(data?.truthFiles) ? data.truthFiles : [],
|
|
186
|
+
delegationFlow: Array.isArray(data?.delegationFlow) ? data.delegationFlow : [],
|
|
187
|
+
roleTypes: Array.isArray(data?.roleTypes) ? data.roleTypes : [],
|
|
188
|
+
artifactTargets: Array.isArray(data?.artifactTargets) ? data.artifactTargets : [],
|
|
189
|
+
reviewConvergence: data?.reviewConvergence || {},
|
|
190
|
+
fallbackMode: data?.fallbackMode || {},
|
|
191
|
+
toolCapabilityMatrix: Array.isArray(data?.toolCapabilityMatrix) ? data.toolCapabilityMatrix : [],
|
|
192
|
+
universalRules: Array.isArray(data?.universalRules) ? data.universalRules : [],
|
|
193
|
+
notes: Array.isArray(data?.notes) ? data.notes : [],
|
|
194
|
+
};
|
|
195
|
+
}
|
package/src/template.js
CHANGED
|
@@ -6,6 +6,45 @@ import { normalizeNewlines } from "./hash.js";
|
|
|
6
6
|
import { extractTemplateBlock, upsertManagedBlock } from "./managed-blocks.js";
|
|
7
7
|
import { UserError } from "./errors.js";
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Extract a leading YAML frontmatter chunk when present.
|
|
11
|
+
*
|
|
12
|
+
* @param {string} text normalized
|
|
13
|
+
* @returns {{ chunk: string, rest: string } | null}
|
|
14
|
+
*/
|
|
15
|
+
function extractLeadingFrontmatter(text) {
|
|
16
|
+
const t = normalizeNewlines(text);
|
|
17
|
+
if (!t.startsWith("---\n")) return null;
|
|
18
|
+
const end = t.indexOf("\n---\n", 4);
|
|
19
|
+
if (end === -1) return null;
|
|
20
|
+
const closeEnd = end + "\n---\n".length;
|
|
21
|
+
return {
|
|
22
|
+
chunk: t.slice(0, closeEnd),
|
|
23
|
+
rest: t.slice(closeEnd),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Keep command metadata in sync without overwriting managed-block files wholesale.
|
|
29
|
+
*
|
|
30
|
+
* @param {string} currentText normalized
|
|
31
|
+
* @param {string} templateText normalized
|
|
32
|
+
* @returns {{ nextText: string, changed: boolean }}
|
|
33
|
+
*/
|
|
34
|
+
function syncLeadingFrontmatter(currentText, templateText) {
|
|
35
|
+
const templateFm = extractLeadingFrontmatter(templateText);
|
|
36
|
+
if (!templateFm) return { nextText: currentText, changed: false };
|
|
37
|
+
|
|
38
|
+
const currentFm = extractLeadingFrontmatter(currentText);
|
|
39
|
+
if (!currentFm) {
|
|
40
|
+
const nextText = `${templateFm.chunk}${currentText}`;
|
|
41
|
+
return { nextText, changed: nextText !== currentText };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const nextText = `${templateFm.chunk}${currentFm.rest}`;
|
|
45
|
+
return { nextText, changed: nextText !== currentText };
|
|
46
|
+
}
|
|
47
|
+
|
|
9
48
|
/**
|
|
10
49
|
* Template file candidates for a given manifest/workspace path.
|
|
11
50
|
*
|
|
@@ -116,8 +155,9 @@ export async function applyManagedBlocksFromTemplate(options) {
|
|
|
116
155
|
}
|
|
117
156
|
|
|
118
157
|
const currentText = normalizeNewlines(await readText(dest));
|
|
119
|
-
|
|
120
|
-
let
|
|
158
|
+
const syncedFrontmatter = syncLeadingFrontmatter(currentText, templateText);
|
|
159
|
+
let nextText = syncedFrontmatter.nextText;
|
|
160
|
+
let anyChanged = syncedFrontmatter.changed;
|
|
121
161
|
|
|
122
162
|
for (const blockId of options.blockIds) {
|
|
123
163
|
const { blockChunk, innerText } = extractTemplateBlock(templateText, blockId);
|