agentxchain 0.8.7 → 2.1.1
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 +123 -154
- package/bin/agentxchain.js +240 -8
- package/dashboard/app.js +305 -0
- package/dashboard/components/blocked.js +145 -0
- package/dashboard/components/cross-repo.js +126 -0
- package/dashboard/components/gate.js +311 -0
- package/dashboard/components/hooks.js +177 -0
- package/dashboard/components/initiative.js +147 -0
- package/dashboard/components/ledger.js +165 -0
- package/dashboard/components/timeline.js +222 -0
- package/dashboard/index.html +352 -0
- package/package.json +16 -7
- package/scripts/agentxchain-autonudge.applescript +32 -5
- package/scripts/live-api-proxy-preflight-smoke.sh +531 -0
- package/scripts/publish-from-tag.sh +88 -0
- package/scripts/release-postflight.sh +231 -0
- package/scripts/release-preflight.sh +167 -0
- package/scripts/run-autonudge.sh +1 -1
- package/src/adapters/claude-code.js +7 -14
- package/src/adapters/cursor-local.js +17 -16
- package/src/commands/accept-turn.js +160 -0
- package/src/commands/approve-completion.js +80 -0
- package/src/commands/approve-transition.js +85 -0
- package/src/commands/branch.js +2 -2
- package/src/commands/claim.js +84 -9
- package/src/commands/config.js +16 -0
- package/src/commands/dashboard.js +70 -0
- package/src/commands/doctor.js +9 -1
- package/src/commands/init.js +540 -5
- package/src/commands/migrate.js +348 -0
- package/src/commands/multi.js +549 -0
- package/src/commands/plugin.js +157 -0
- package/src/commands/reject-turn.js +204 -0
- package/src/commands/resume.js +389 -0
- package/src/commands/status.js +196 -3
- package/src/commands/step.js +947 -0
- package/src/commands/stop.js +65 -33
- package/src/commands/template-list.js +33 -0
- package/src/commands/template-set.js +279 -0
- package/src/commands/update.js +24 -3
- package/src/commands/validate.js +20 -11
- package/src/commands/verify.js +71 -0
- package/src/commands/watch.js +112 -25
- package/src/lib/adapters/api-proxy-adapter.js +1076 -0
- package/src/lib/adapters/local-cli-adapter.js +337 -0
- package/src/lib/adapters/manual-adapter.js +169 -0
- package/src/lib/blocked-state.js +94 -0
- package/src/lib/config.js +143 -12
- package/src/lib/context-compressor.js +121 -0
- package/src/lib/context-section-parser.js +220 -0
- package/src/lib/coordinator-acceptance.js +428 -0
- package/src/lib/coordinator-config.js +461 -0
- package/src/lib/coordinator-dispatch.js +276 -0
- package/src/lib/coordinator-gates.js +487 -0
- package/src/lib/coordinator-hooks.js +239 -0
- package/src/lib/coordinator-recovery.js +523 -0
- package/src/lib/coordinator-state.js +365 -0
- package/src/lib/cross-repo-context.js +247 -0
- package/src/lib/dashboard/bridge-server.js +284 -0
- package/src/lib/dashboard/file-watcher.js +93 -0
- package/src/lib/dashboard/state-reader.js +96 -0
- package/src/lib/dispatch-bundle.js +568 -0
- package/src/lib/dispatch-manifest.js +252 -0
- package/src/lib/filter-agents.js +12 -0
- package/src/lib/gate-evaluator.js +285 -0
- package/src/lib/generate-vscode.js +158 -68
- package/src/lib/governed-state.js +2139 -0
- package/src/lib/governed-templates.js +145 -0
- package/src/lib/hook-runner.js +788 -0
- package/src/lib/next-owner.js +61 -6
- package/src/lib/normalized-config.js +539 -0
- package/src/lib/notify.js +14 -12
- package/src/lib/plugin-config-schema.js +192 -0
- package/src/lib/plugins.js +692 -0
- package/src/lib/prompt-core.js +108 -0
- package/src/lib/protocol-conformance.js +291 -0
- package/src/lib/reference-conformance-adapter.js +717 -0
- package/src/lib/repo-observer.js +597 -0
- package/src/lib/repo.js +0 -31
- package/src/lib/safe-write.js +44 -0
- package/src/lib/schema.js +189 -0
- package/src/lib/schemas/turn-result.schema.json +205 -0
- package/src/lib/seed-prompt-polling.js +15 -73
- package/src/lib/seed-prompt.js +17 -63
- package/src/lib/token-budget.js +206 -0
- package/src/lib/token-counter.js +27 -0
- package/src/lib/turn-paths.js +67 -0
- package/src/lib/turn-result-validator.js +496 -0
- package/src/lib/validation.js +167 -19
- package/src/lib/verify-command.js +72 -0
- package/src/templates/governed/api-service.json +31 -0
- package/src/templates/governed/cli-tool.json +30 -0
- package/src/templates/governed/generic.json +10 -0
- package/src/templates/governed/web-app.json +30 -0
package/src/lib/config.js
CHANGED
|
@@ -1,36 +1,167 @@
|
|
|
1
1
|
import { readFileSync, existsSync } from 'fs';
|
|
2
|
-
import { join } from 'path';
|
|
2
|
+
import { join, parse as pathParse, resolve } from 'path';
|
|
3
|
+
import { safeParseJson, validateConfigSchema, validateLockSchema, validateProjectStateSchema, validateStateSchema } from './schema.js';
|
|
4
|
+
import { loadNormalizedConfig } from './normalized-config.js';
|
|
5
|
+
import { safeWriteJson } from './safe-write.js';
|
|
6
|
+
import { normalizeGovernedStateShape, getActiveTurn } from './governed-state.js';
|
|
7
|
+
|
|
8
|
+
function attachLegacyCurrentTurnAlias(state) {
|
|
9
|
+
if (!state || typeof state !== 'object') {
|
|
10
|
+
return state;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const existing = Object.getOwnPropertyDescriptor(state, 'current_turn');
|
|
14
|
+
if (existing && existing.enumerable === false) {
|
|
15
|
+
return state;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
Object.defineProperty(state, 'current_turn', {
|
|
19
|
+
configurable: true,
|
|
20
|
+
enumerable: false,
|
|
21
|
+
get() {
|
|
22
|
+
return getActiveTurn(state);
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
return state;
|
|
27
|
+
}
|
|
3
28
|
|
|
4
29
|
const CONFIG_FILE = 'agentxchain.json';
|
|
5
30
|
const LOCK_FILE = 'lock.json';
|
|
6
31
|
const STATE_FILE = 'state.json';
|
|
7
32
|
|
|
8
33
|
export function findProjectRoot(startDir = process.cwd()) {
|
|
9
|
-
let dir = startDir;
|
|
10
|
-
|
|
34
|
+
let dir = resolve(startDir);
|
|
35
|
+
const { root: fsRoot } = pathParse(dir);
|
|
36
|
+
while (true) {
|
|
11
37
|
if (existsSync(join(dir, CONFIG_FILE))) return dir;
|
|
38
|
+
if (dir === fsRoot) return null;
|
|
12
39
|
dir = join(dir, '..');
|
|
13
40
|
}
|
|
14
|
-
return null;
|
|
15
41
|
}
|
|
16
42
|
|
|
17
43
|
export function loadConfig(dir = process.cwd()) {
|
|
18
44
|
const root = findProjectRoot(dir);
|
|
19
45
|
if (!root) return null;
|
|
20
|
-
const
|
|
21
|
-
|
|
46
|
+
const filePath = join(root, CONFIG_FILE);
|
|
47
|
+
let raw;
|
|
48
|
+
try {
|
|
49
|
+
raw = readFileSync(filePath, 'utf8');
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
const result = safeParseJson(raw, validateConfigSchema);
|
|
54
|
+
if (!result.ok) {
|
|
55
|
+
console.error(` Warning: agentxchain.json has issues: ${result.errors.join(', ')}`);
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
return { root, config: result.data };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function loadProjectContext(dir = process.cwd()) {
|
|
62
|
+
const root = findProjectRoot(dir);
|
|
63
|
+
if (!root) return null;
|
|
64
|
+
|
|
65
|
+
const filePath = join(root, CONFIG_FILE);
|
|
66
|
+
let raw;
|
|
67
|
+
try {
|
|
68
|
+
raw = readFileSync(filePath, 'utf8');
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let parsed;
|
|
74
|
+
try {
|
|
75
|
+
parsed = JSON.parse(raw);
|
|
76
|
+
} catch (err) {
|
|
77
|
+
console.error(` Warning: agentxchain.json is invalid JSON: ${err.message}`);
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const normalized = loadNormalizedConfig(parsed, root);
|
|
82
|
+
if (!normalized.ok) {
|
|
83
|
+
console.error(` Warning: agentxchain.json has issues: ${normalized.errors.join(', ')}`);
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
root,
|
|
89
|
+
rawConfig: parsed,
|
|
90
|
+
config: normalized.normalized,
|
|
91
|
+
version: normalized.version,
|
|
92
|
+
};
|
|
22
93
|
}
|
|
23
94
|
|
|
24
95
|
export function loadLock(root) {
|
|
25
|
-
const
|
|
26
|
-
if (!existsSync(
|
|
27
|
-
|
|
96
|
+
const filePath = join(root, LOCK_FILE);
|
|
97
|
+
if (!existsSync(filePath)) return null;
|
|
98
|
+
let raw;
|
|
99
|
+
try {
|
|
100
|
+
raw = readFileSync(filePath, 'utf8');
|
|
101
|
+
} catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
const result = safeParseJson(raw, validateLockSchema);
|
|
105
|
+
if (!result.ok) {
|
|
106
|
+
console.error(` Warning: lock.json has issues: ${result.errors.join(', ')}`);
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
return result.data;
|
|
28
110
|
}
|
|
29
111
|
|
|
30
112
|
export function loadState(root) {
|
|
31
|
-
const
|
|
32
|
-
if (!existsSync(
|
|
33
|
-
|
|
113
|
+
const filePath = join(root, STATE_FILE);
|
|
114
|
+
if (!existsSync(filePath)) return null;
|
|
115
|
+
let raw;
|
|
116
|
+
try {
|
|
117
|
+
raw = readFileSync(filePath, 'utf8');
|
|
118
|
+
} catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
const result = safeParseJson(raw, validateStateSchema);
|
|
122
|
+
if (!result.ok) {
|
|
123
|
+
console.error(` Warning: state.json has issues: ${result.errors.join(', ')}`);
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
return result.data;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function loadProjectState(root, config) {
|
|
130
|
+
const relPath = config?.files?.state || STATE_FILE;
|
|
131
|
+
const filePath = join(root, relPath);
|
|
132
|
+
if (!existsSync(filePath)) return null;
|
|
133
|
+
|
|
134
|
+
let raw;
|
|
135
|
+
try {
|
|
136
|
+
raw = readFileSync(filePath, 'utf8');
|
|
137
|
+
} catch {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const parsed = safeParseJson(raw);
|
|
142
|
+
if (!parsed.ok) {
|
|
143
|
+
console.error(` Warning: ${relPath} has issues: ${parsed.errors.join(', ')}`);
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let stateData = parsed.data;
|
|
148
|
+
if (config?.protocol_mode === 'governed') {
|
|
149
|
+
const normalized = normalizeGovernedStateShape(stateData);
|
|
150
|
+
stateData = normalized.state;
|
|
151
|
+
if (normalized.changed) {
|
|
152
|
+
safeWriteJson(filePath, stateData);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const result = validateProjectStateSchema(stateData);
|
|
157
|
+
if (!result.ok) {
|
|
158
|
+
console.error(` Warning: ${relPath} has issues: ${result.errors.join(', ')}`);
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
if (config?.protocol_mode === 'governed') {
|
|
162
|
+
return attachLegacyCurrentTurnAlias(stateData);
|
|
163
|
+
}
|
|
164
|
+
return stateData;
|
|
34
165
|
}
|
|
35
166
|
|
|
36
167
|
export { CONFIG_FILE, LOCK_FILE, STATE_FILE };
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded context compressor for preflight tokenization.
|
|
3
|
+
*
|
|
4
|
+
* Takes parsed context sections and applies the v1.1 deterministic
|
|
5
|
+
* compression order from PREEMPTIVE_TOKENIZATION_SPEC.md §4.
|
|
6
|
+
* Does not perform token counting — that is the caller's responsibility.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { renderContextSections } from './context-section-parser.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The fixed compression order. Each step is tried in sequence until the
|
|
13
|
+
* caller's budget check passes or all steps are exhausted.
|
|
14
|
+
*/
|
|
15
|
+
const COMPRESSION_STEPS = [
|
|
16
|
+
{ id: 'budget', action: 'drop' },
|
|
17
|
+
{ id: 'phase_gate_status', action: 'drop' },
|
|
18
|
+
{ id: 'gate_required_files', action: 'drop' },
|
|
19
|
+
{ id: 'last_turn_objections', action: 'drop' },
|
|
20
|
+
{ id: 'last_turn_decisions', action: 'drop' },
|
|
21
|
+
{ id: 'last_turn_summary', action: 'truncate', max_chars: 240 },
|
|
22
|
+
{ id: 'last_turn_summary', action: 'drop' },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
export { COMPRESSION_STEPS };
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @typedef {Object} SectionAction
|
|
29
|
+
* @property {string} id - Section ID
|
|
30
|
+
* @property {boolean} required - Whether the section is sticky
|
|
31
|
+
* @property {number} original_tokens - Token count before compression (set by caller)
|
|
32
|
+
* @property {number} final_tokens - Token count after compression (set by caller)
|
|
33
|
+
* @property {'kept' | 'dropped' | 'truncated'} action - What happened to this section
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @typedef {Object} CompressionResult
|
|
38
|
+
* @property {Array<Object>} sections - The (possibly compressed) section array
|
|
39
|
+
* @property {Array<SectionAction>} actions - Per-section action log
|
|
40
|
+
* @property {string} effective_context - Rendered markdown of the compressed sections
|
|
41
|
+
* @property {boolean} exhausted - True if all compression steps were applied
|
|
42
|
+
* @property {number} steps_applied - Number of compression steps applied
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Compress parsed context sections using the v1.1 bounded compression order.
|
|
47
|
+
*
|
|
48
|
+
* The `fitsInBudget` callback is called after each compression step with the
|
|
49
|
+
* current effective context string. It should return true if the full outbound
|
|
50
|
+
* request (including system prompt, PROMPT.md, separator, and the provided
|
|
51
|
+
* effective context) fits within the available input token budget.
|
|
52
|
+
*
|
|
53
|
+
* @param {Array<Object>} sections - Parsed sections from parseContextSections()
|
|
54
|
+
* @param {(effectiveContext: string) => boolean} fitsInBudget - Budget check callback
|
|
55
|
+
* @returns {CompressionResult}
|
|
56
|
+
*/
|
|
57
|
+
export function compressContextSections(sections, fitsInBudget) {
|
|
58
|
+
// Deep copy so we don't mutate the caller's array
|
|
59
|
+
let working = sections.map((s) => ({ ...s }));
|
|
60
|
+
|
|
61
|
+
// Track actions per section
|
|
62
|
+
const actionMap = new Map(
|
|
63
|
+
sections.map((s) => [s.id, { id: s.id, required: s.required, action: 'kept' }])
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
// Check if it already fits before any compression
|
|
67
|
+
let effectiveContext = renderContextSections(working);
|
|
68
|
+
if (fitsInBudget(effectiveContext)) {
|
|
69
|
+
return buildResult(working, actionMap, effectiveContext, false, 0);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Apply compression steps in order
|
|
73
|
+
let stepsApplied = 0;
|
|
74
|
+
for (const step of COMPRESSION_STEPS) {
|
|
75
|
+
const sectionIndex = working.findIndex((s) => s.id === step.id);
|
|
76
|
+
if (sectionIndex === -1) {
|
|
77
|
+
// Section not present — skip this step
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const section = working[sectionIndex];
|
|
82
|
+
|
|
83
|
+
// Safety: never compress a required/sticky section
|
|
84
|
+
if (section.required) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (step.action === 'drop') {
|
|
89
|
+
working = working.filter((s) => s.id !== step.id);
|
|
90
|
+
actionMap.get(step.id).action = 'dropped';
|
|
91
|
+
stepsApplied += 1;
|
|
92
|
+
} else if (step.action === 'truncate') {
|
|
93
|
+
const original = section.content;
|
|
94
|
+
if (original.length > step.max_chars) {
|
|
95
|
+
section.content = original.slice(0, step.max_chars);
|
|
96
|
+
// Only count as a step if we actually truncated
|
|
97
|
+
actionMap.get(step.id).action = 'truncated';
|
|
98
|
+
stepsApplied += 1;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
effectiveContext = renderContextSections(working);
|
|
103
|
+
if (fitsInBudget(effectiveContext)) {
|
|
104
|
+
return buildResult(working, actionMap, effectiveContext, false, stepsApplied);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// All compression steps exhausted and still doesn't fit
|
|
109
|
+
effectiveContext = renderContextSections(working);
|
|
110
|
+
return buildResult(working, actionMap, effectiveContext, true, stepsApplied);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function buildResult(sections, actionMap, effectiveContext, exhausted, stepsApplied) {
|
|
114
|
+
return {
|
|
115
|
+
sections,
|
|
116
|
+
actions: Array.from(actionMap.values()),
|
|
117
|
+
effective_context: effectiveContext,
|
|
118
|
+
exhausted,
|
|
119
|
+
steps_applied: stepsApplied,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
const CONTEXT_TITLE = '# Execution Context';
|
|
2
|
+
|
|
3
|
+
const SECTION_DEFINITIONS = [
|
|
4
|
+
{ id: 'current_state', header: 'Current State', required: true },
|
|
5
|
+
{ id: 'budget', header: null, required: false },
|
|
6
|
+
{ id: 'last_turn_header', header: 'Last Accepted Turn', required: true },
|
|
7
|
+
{ id: 'last_turn_summary', header: null, required: false },
|
|
8
|
+
{ id: 'last_turn_decisions', header: null, required: false },
|
|
9
|
+
{ id: 'last_turn_objections', header: null, required: false },
|
|
10
|
+
{ id: 'blockers', header: 'Blockers', required: true },
|
|
11
|
+
{ id: 'escalation', header: 'Escalation', required: true },
|
|
12
|
+
{ id: 'gate_required_files', header: 'Gate Required Files', required: false },
|
|
13
|
+
{ id: 'phase_gate_status', header: 'Phase Gate Status', required: false },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const REQUIRED_BY_ID = new Map(SECTION_DEFINITIONS.map((section) => [section.id, section.required]));
|
|
17
|
+
const HEADER_TO_ID = new Map(
|
|
18
|
+
SECTION_DEFINITIONS
|
|
19
|
+
.filter((section) => section.header)
|
|
20
|
+
.map((section) => [section.header, section.id])
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const BUDGET_LINE_PATTERN = /^- \*\*Budget (spent|remaining):\*\*/;
|
|
24
|
+
const SUMMARY_LINE_PATTERN = /^- \*\*Summary:\*\*/;
|
|
25
|
+
const DECISIONS_LINE_PATTERN = /^- \*\*Decisions:\*\*/;
|
|
26
|
+
const OBJECTIONS_LINE_PATTERN = /^- \*\*Objections:\*\*/;
|
|
27
|
+
|
|
28
|
+
export { CONTEXT_TITLE, SECTION_DEFINITIONS };
|
|
29
|
+
|
|
30
|
+
export function parseContextSections(contextMd) {
|
|
31
|
+
const normalized = normalizeNewlines(contextMd);
|
|
32
|
+
const topLevelSections = splitTopLevelSections(normalized);
|
|
33
|
+
const parsedSections = [];
|
|
34
|
+
|
|
35
|
+
const currentStateBody = topLevelSections.get('Current State');
|
|
36
|
+
if (currentStateBody) {
|
|
37
|
+
const budgetLines = currentStateBody.filter((line) => BUDGET_LINE_PATTERN.test(line));
|
|
38
|
+
const stickyLines = currentStateBody.filter((line) => !BUDGET_LINE_PATTERN.test(line));
|
|
39
|
+
|
|
40
|
+
pushSection(parsedSections, 'current_state', stickyLines);
|
|
41
|
+
pushSection(parsedSections, 'budget', budgetLines);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const lastAcceptedTurnBody = topLevelSections.get('Last Accepted Turn');
|
|
45
|
+
if (lastAcceptedTurnBody) {
|
|
46
|
+
const {
|
|
47
|
+
headerLines,
|
|
48
|
+
summaryLines,
|
|
49
|
+
decisionsLines,
|
|
50
|
+
objectionsLines,
|
|
51
|
+
} = splitLastAcceptedTurn(lastAcceptedTurnBody);
|
|
52
|
+
|
|
53
|
+
pushSection(parsedSections, 'last_turn_header', headerLines);
|
|
54
|
+
pushSection(parsedSections, 'last_turn_summary', summaryLines);
|
|
55
|
+
pushSection(parsedSections, 'last_turn_decisions', decisionsLines);
|
|
56
|
+
pushSection(parsedSections, 'last_turn_objections', objectionsLines);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
for (const [header, id] of HEADER_TO_ID.entries()) {
|
|
60
|
+
if (header === 'Current State' || header === 'Last Accepted Turn') continue;
|
|
61
|
+
pushSection(parsedSections, id, topLevelSections.get(header));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return SECTION_DEFINITIONS
|
|
65
|
+
.map((definition) => parsedSections.find((section) => section.id === definition.id))
|
|
66
|
+
.filter(Boolean);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function renderContextSections(sections) {
|
|
70
|
+
const sectionMap = new Map((sections || []).map((section) => [section.id, section]));
|
|
71
|
+
const lines = [CONTEXT_TITLE, ''];
|
|
72
|
+
|
|
73
|
+
appendTopLevelSection(lines, 'Current State', [
|
|
74
|
+
sectionMap.get('current_state')?.content,
|
|
75
|
+
sectionMap.get('budget')?.content,
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
appendTopLevelSection(lines, 'Last Accepted Turn', [
|
|
79
|
+
sectionMap.get('last_turn_header')?.content,
|
|
80
|
+
sectionMap.get('last_turn_summary')?.content,
|
|
81
|
+
sectionMap.get('last_turn_decisions')?.content,
|
|
82
|
+
sectionMap.get('last_turn_objections')?.content,
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
appendTopLevelSection(lines, 'Blockers', [sectionMap.get('blockers')?.content]);
|
|
86
|
+
appendTopLevelSection(lines, 'Escalation', [sectionMap.get('escalation')?.content]);
|
|
87
|
+
appendTopLevelSection(lines, 'Gate Required Files', [sectionMap.get('gate_required_files')?.content]);
|
|
88
|
+
appendTopLevelSection(lines, 'Phase Gate Status', [sectionMap.get('phase_gate_status')?.content]);
|
|
89
|
+
|
|
90
|
+
return `${lines.join('\n')}\n`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function appendTopLevelSection(lines, header, fragments) {
|
|
94
|
+
const content = fragments
|
|
95
|
+
.filter((fragment) => typeof fragment === 'string' && fragment.length > 0)
|
|
96
|
+
.join('\n');
|
|
97
|
+
|
|
98
|
+
if (!content) return;
|
|
99
|
+
|
|
100
|
+
lines.push(`## ${header}`);
|
|
101
|
+
lines.push('');
|
|
102
|
+
lines.push(...content.split('\n'));
|
|
103
|
+
lines.push('');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function splitTopLevelSections(contextMd) {
|
|
107
|
+
const lines = normalizeNewlines(contextMd).split('\n');
|
|
108
|
+
const sectionStarts = [];
|
|
109
|
+
|
|
110
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
111
|
+
if (lines[index].startsWith('## ')) {
|
|
112
|
+
sectionStarts.push(index);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const sections = new Map();
|
|
117
|
+
for (let index = 0; index < sectionStarts.length; index += 1) {
|
|
118
|
+
const start = sectionStarts[index];
|
|
119
|
+
const end = sectionStarts[index + 1] ?? lines.length;
|
|
120
|
+
const header = lines[start].slice(3).trim();
|
|
121
|
+
const bodyLines = trimBlankLines(lines.slice(start + 1, end));
|
|
122
|
+
sections.set(header, bodyLines);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return sections;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function splitLastAcceptedTurn(lines) {
|
|
129
|
+
const headerLines = [];
|
|
130
|
+
let summaryLines = [];
|
|
131
|
+
let decisionsLines = [];
|
|
132
|
+
let objectionsLines = [];
|
|
133
|
+
|
|
134
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
135
|
+
const line = lines[index];
|
|
136
|
+
|
|
137
|
+
if (SUMMARY_LINE_PATTERN.test(line)) {
|
|
138
|
+
summaryLines = [line];
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (DECISIONS_LINE_PATTERN.test(line)) {
|
|
143
|
+
const { blockLines, nextIndex } = consumeIndentedBlock(lines, index);
|
|
144
|
+
decisionsLines = blockLines;
|
|
145
|
+
index = nextIndex - 1;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (OBJECTIONS_LINE_PATTERN.test(line)) {
|
|
150
|
+
const { blockLines, nextIndex } = consumeIndentedBlock(lines, index);
|
|
151
|
+
objectionsLines = blockLines;
|
|
152
|
+
index = nextIndex - 1;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
headerLines.push(line);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
headerLines: trimBlankLines(headerLines),
|
|
161
|
+
summaryLines: trimBlankLines(summaryLines),
|
|
162
|
+
decisionsLines: trimBlankLines(decisionsLines),
|
|
163
|
+
objectionsLines: trimBlankLines(objectionsLines),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function consumeIndentedBlock(lines, startIndex) {
|
|
168
|
+
const blockLines = [lines[startIndex]];
|
|
169
|
+
let index = startIndex + 1;
|
|
170
|
+
|
|
171
|
+
while (index < lines.length) {
|
|
172
|
+
const line = lines[index];
|
|
173
|
+
if (line.startsWith(' ') || line.trim() === '') {
|
|
174
|
+
blockLines.push(line);
|
|
175
|
+
index += 1;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
blockLines: trimBlankLines(blockLines),
|
|
183
|
+
nextIndex: index,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function pushSection(target, id, lines) {
|
|
188
|
+
const normalizedLines = trimBlankLines(lines || []);
|
|
189
|
+
if (!normalizedLines.length) return;
|
|
190
|
+
|
|
191
|
+
target.push({
|
|
192
|
+
id,
|
|
193
|
+
content: normalizedLines.join('\n'),
|
|
194
|
+
required: REQUIRED_BY_ID.get(id) === true,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function trimBlankLines(lines) {
|
|
199
|
+
return trimTrailingBlankLines(trimLeadingBlankLines(lines));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function trimLeadingBlankLines(lines) {
|
|
203
|
+
let start = 0;
|
|
204
|
+
while (start < lines.length && lines[start].trim() === '') {
|
|
205
|
+
start += 1;
|
|
206
|
+
}
|
|
207
|
+
return lines.slice(start);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function trimTrailingBlankLines(lines) {
|
|
211
|
+
let end = lines.length;
|
|
212
|
+
while (end > 0 && lines[end - 1].trim() === '') {
|
|
213
|
+
end -= 1;
|
|
214
|
+
}
|
|
215
|
+
return lines.slice(0, end);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function normalizeNewlines(value) {
|
|
219
|
+
return String(value || '').replace(/\r\n/g, '\n');
|
|
220
|
+
}
|