@aperant/framework 0.6.5 → 0.6.7
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/CHANGELOG.md +155 -0
- package/README.md +17 -251
- package/dist/cli/commands/check-version.d.mts.map +1 -1
- package/dist/cli/commands/check-version.mjs +76 -1
- package/dist/cli/commands/check-version.mjs.map +1 -1
- package/dist/cli/commands/ci-watch.d.mts.map +1 -1
- package/dist/cli/commands/ci-watch.mjs +34 -4
- package/dist/cli/commands/ci-watch.mjs.map +1 -1
- package/dist/cli/commands/commit.d.mts.map +1 -1
- package/dist/cli/commands/commit.mjs +25 -8
- package/dist/cli/commands/commit.mjs.map +1 -1
- package/dist/cli/commands/event.d.mts.map +1 -1
- package/dist/cli/commands/event.mjs +66 -0
- package/dist/cli/commands/event.mjs.map +1 -1
- package/dist/cli/commands/init.d.mts +8 -1
- package/dist/cli/commands/init.d.mts.map +1 -1
- package/dist/cli/commands/init.mjs +91 -9
- package/dist/cli/commands/init.mjs.map +1 -1
- package/dist/cli/commands/modes.d.mts.map +1 -1
- package/dist/cli/commands/modes.mjs +2 -1
- package/dist/cli/commands/modes.mjs.map +1 -1
- package/dist/cli/commands/release-notes.d.mts +11 -0
- package/dist/cli/commands/release-notes.d.mts.map +1 -0
- package/dist/cli/commands/release-notes.mjs +173 -0
- package/dist/cli/commands/release-notes.mjs.map +1 -0
- package/dist/cli/commands/task.d.mts.map +1 -1
- package/dist/cli/commands/task.mjs +285 -139
- package/dist/cli/commands/task.mjs.map +1 -1
- package/dist/cli/commands/triage.d.mts.map +1 -1
- package/dist/cli/commands/triage.mjs +9 -5
- package/dist/cli/commands/triage.mjs.map +1 -1
- package/dist/cli/config/load.d.mts +14 -0
- package/dist/cli/config/load.d.mts.map +1 -1
- package/dist/cli/config/load.mjs +40 -0
- package/dist/cli/config/load.mjs.map +1 -1
- package/dist/cli/config/upgrade-gitignore.d.mts.map +1 -1
- package/dist/cli/config/upgrade-gitignore.mjs +6 -0
- package/dist/cli/config/upgrade-gitignore.mjs.map +1 -1
- package/dist/cli/consistency/parse-review.d.mts.map +1 -1
- package/dist/cli/consistency/parse-review.mjs +4 -1
- package/dist/cli/consistency/parse-review.mjs.map +1 -1
- package/dist/cli/coordination/auto-emit-artifact-ready.d.mts +16 -0
- package/dist/cli/coordination/auto-emit-artifact-ready.d.mts.map +1 -0
- package/dist/cli/coordination/auto-emit-artifact-ready.mjs +88 -0
- package/dist/cli/coordination/auto-emit-artifact-ready.mjs.map +1 -0
- package/dist/cli/coordination/event-schema.d.mts +16 -0
- package/dist/cli/coordination/event-schema.d.mts.map +1 -0
- package/dist/cli/coordination/event-schema.mjs +161 -0
- package/dist/cli/coordination/event-schema.mjs.map +1 -0
- package/dist/cli/coordination/store.d.mts +6 -0
- package/dist/cli/coordination/store.d.mts.map +1 -1
- package/dist/cli/coordination/store.mjs +14 -0
- package/dist/cli/coordination/store.mjs.map +1 -1
- package/dist/cli/dispatch.d.mts.map +1 -1
- package/dist/cli/dispatch.mjs +2 -0
- package/dist/cli/dispatch.mjs.map +1 -1
- package/dist/cli/gate/gates/review-clean.d.mts.map +1 -1
- package/dist/cli/gate/gates/review-clean.mjs +4 -2
- package/dist/cli/gate/gates/review-clean.mjs.map +1 -1
- package/dist/cli/help.mjs +2 -2
- package/dist/cli/help.mjs.map +1 -1
- package/dist/cli/install/update-chips.d.mts +23 -0
- package/dist/cli/install/update-chips.d.mts.map +1 -1
- package/dist/cli/install/update-chips.mjs +60 -0
- package/dist/cli/install/update-chips.mjs.map +1 -1
- package/dist/cli/release-notes/compile.d.mts +38 -0
- package/dist/cli/release-notes/compile.d.mts.map +1 -0
- package/dist/cli/release-notes/compile.mjs +244 -0
- package/dist/cli/release-notes/compile.mjs.map +1 -0
- package/dist/cli/release-notes/draft.d.mts +73 -0
- package/dist/cli/release-notes/draft.d.mts.map +1 -0
- package/dist/cli/release-notes/draft.mjs +120 -0
- package/dist/cli/release-notes/draft.mjs.map +1 -0
- package/dist/cli/release-notes/output-dir.d.mts +20 -0
- package/dist/cli/release-notes/output-dir.d.mts.map +1 -0
- package/dist/cli/release-notes/output-dir.mjs +42 -0
- package/dist/cli/release-notes/output-dir.mjs.map +1 -0
- package/dist/cli/release-notes/persona-filter.d.mts +51 -0
- package/dist/cli/release-notes/persona-filter.d.mts.map +1 -0
- package/dist/cli/release-notes/persona-filter.mjs +127 -0
- package/dist/cli/release-notes/persona-filter.mjs.map +1 -0
- package/dist/cli/release-notes/publish.d.mts +23 -0
- package/dist/cli/release-notes/publish.d.mts.map +1 -0
- package/dist/cli/release-notes/publish.mjs +125 -0
- package/dist/cli/release-notes/publish.mjs.map +1 -0
- package/dist/cli/release-notes/ship-autodraft.d.mts +37 -0
- package/dist/cli/release-notes/ship-autodraft.d.mts.map +1 -0
- package/dist/cli/release-notes/ship-autodraft.mjs +97 -0
- package/dist/cli/release-notes/ship-autodraft.mjs.map +1 -0
- package/dist/cli/roadmap/conductor-view.d.mts +13 -0
- package/dist/cli/roadmap/conductor-view.d.mts.map +1 -0
- package/dist/cli/roadmap/conductor-view.mjs +31 -0
- package/dist/cli/roadmap/conductor-view.mjs.map +1 -0
- package/dist/cli/route/skill-discover.d.mts.map +1 -1
- package/dist/cli/route/skill-discover.mjs +2 -1
- package/dist/cli/route/skill-discover.mjs.map +1 -1
- package/dist/cli/task/ids.d.mts +7 -4
- package/dist/cli/task/ids.d.mts.map +1 -1
- package/dist/cli/task/ids.mjs +11 -10
- package/dist/cli/task/ids.mjs.map +1 -1
- package/dist/types/config.d.ts +11 -7
- package/dist/types/config.d.ts.map +1 -1
- package/package.json +9 -1
- package/prompts/conductor-status-check.md +23 -0
- package/prompts/conductor-sub-agent.md +57 -0
- package/prompts/conductor-system.md +172 -0
- package/skills/apt-close-task/SKILL.md +25 -0
- package/skills/apt-diagram/SKILL.md +45 -9
- package/skills/apt-plan/SKILL.md +12 -0
- package/skills/apt-plan/adapters/conductor.md +98 -0
- package/skills/apt-release-notes/SKILL.md +193 -0
- package/skills/apt-release-notes/appendices/persona-voice.md +59 -0
- package/skills/apt-setup/SKILL.md +148 -3
- package/skills/apt-ship/SKILL.md +74 -12
- package/skills/apt-spar/SKILL.md +290 -0
- package/skills/apt-update/SKILL.md +51 -9
- package/skills/apt-watch-ci/SKILL.md +2 -2
- package/src/cli/commands/check-version.mjs +73 -1
- package/src/cli/commands/ci-watch.mjs +35 -4
- package/src/cli/commands/commit.mjs +27 -8
- package/src/cli/commands/event.mjs +68 -0
- package/src/cli/commands/init.mjs +101 -9
- package/src/cli/commands/modes.mjs +2 -1
- package/src/cli/commands/release-notes.mjs +187 -0
- package/src/cli/commands/task.mjs +305 -152
- package/src/cli/commands/triage.mjs +14 -5
- package/src/cli/config/load.mjs +37 -0
- package/src/cli/config/upgrade-gitignore.mjs +6 -0
- package/src/cli/consistency/parse-review.mjs +3 -1
- package/src/cli/coordination/auto-emit-artifact-ready.mjs +96 -0
- package/src/cli/coordination/event-schema.d.ts +13 -0
- package/src/cli/coordination/event-schema.mjs +174 -0
- package/src/cli/coordination/store.mjs +14 -0
- package/src/cli/dispatch.mjs +2 -0
- package/src/cli/gate/gates/review-clean.mjs +4 -2
- package/src/cli/help.mjs +2 -2
- package/src/cli/install/update-chips.mjs +57 -0
- package/src/cli/release-notes/compile.mjs +261 -0
- package/src/cli/release-notes/draft.mjs +133 -0
- package/src/cli/release-notes/output-dir.mjs +52 -0
- package/src/cli/release-notes/persona-filter.mjs +126 -0
- package/src/cli/release-notes/publish.mjs +128 -0
- package/src/cli/release-notes/ship-autodraft.mjs +106 -0
- package/src/cli/roadmap/conductor-view.d.ts +10 -0
- package/src/cli/roadmap/conductor-view.mjs +31 -0
- package/src/cli/route/skill-discover.mjs +2 -1
- package/src/cli/task/ids.mjs +15 -13
- package/templates/config.json +28 -3
- package/workflows/docs.md +12 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @internal
|
|
3
|
+
* auto-emit-artifact-ready.mjs — code-enforced plan-ready signal.
|
|
4
|
+
*
|
|
5
|
+
* Promotes the `artifact.ready{kind:'plan'}` emit from a markdown contract
|
|
6
|
+
* in `skills/apt-plan/adapters/conductor.md` to a framework-level
|
|
7
|
+
* postcondition of `apt-tools commit`. This closes the Conductor v2 gap
|
|
8
|
+
* exposed during live verification 2026-05-15: the planner's QUICK /
|
|
9
|
+
* SIMPLE track is hard-exempt from "new interaction discipline"
|
|
10
|
+
* (Pocock Fast Path Guarantee ID-05), so the markdown emit step was
|
|
11
|
+
* never executed for tasks router-classified as simple. The fleet
|
|
12
|
+
* never saw artifact.ready, the PlanReadyCard never auto-promoted.
|
|
13
|
+
*
|
|
14
|
+
* The fix lives in `framework` rather than the skill so:
|
|
15
|
+
* - it works for QUICK + STANDARD + DEEP + COMPLEX uniformly
|
|
16
|
+
* - it doesn't violate the Fast Path Guarantee (a passive log emit
|
|
17
|
+
* is not interaction discipline)
|
|
18
|
+
* - it's the same code path every CLI consumer (Claude Code, Gemini,
|
|
19
|
+
* OpenCode, Codex) traverses via `apt-tools commit`
|
|
20
|
+
*
|
|
21
|
+
* Conditions for emit (ALL must hold — otherwise no-op):
|
|
22
|
+
* 1. `APERANT_TERMINAL_ID` is set in process.env (Conductor PTY context).
|
|
23
|
+
* 2. The committed files include a path matching
|
|
24
|
+
* `.aperant/tasks/{task-id}/implementation_plan.json`.
|
|
25
|
+
*
|
|
26
|
+
* The emit re-uses the same `event-schema` validator and JSONL writer
|
|
27
|
+
* `commands/event.mjs` already uses, so the envelope structure stays
|
|
28
|
+
* byte-identical with manually-emitted ones.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { createHash } from 'node:crypto'
|
|
32
|
+
import { appendFileSync, mkdirSync, readFileSync } from 'node:fs'
|
|
33
|
+
import { hostname } from 'node:os'
|
|
34
|
+
import { join } from 'node:path'
|
|
35
|
+
import { validateConductorEvent } from './event-schema.mjs'
|
|
36
|
+
|
|
37
|
+
const PLAN_PATH_RE = /^(?:\.\/)?\.aperant\/tasks\/([^/]+)\/implementation_plan\.json$/
|
|
38
|
+
|
|
39
|
+
let _incarnationCounter = 0
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {{ files: string[], projectRoot: string }} input
|
|
43
|
+
* @returns {{ emitted: false, reason: string } | { emitted: true, envelope: object, file: string }}
|
|
44
|
+
*/
|
|
45
|
+
export function maybeAutoEmitPlanReady({ files, projectRoot }) {
|
|
46
|
+
const terminalId = process.env.APERANT_TERMINAL_ID
|
|
47
|
+
if (!terminalId) return { emitted: false, reason: 'APERANT_TERMINAL_ID unset' }
|
|
48
|
+
|
|
49
|
+
const match = files.map((f) => f.replace(/\\/g, '/').match(PLAN_PATH_RE)).find((m) => m !== null)
|
|
50
|
+
if (!match) return { emitted: false, reason: 'no implementation_plan.json in committed files' }
|
|
51
|
+
const taskId = match[1]
|
|
52
|
+
|
|
53
|
+
const planRelPath = `.aperant/tasks/${taskId}/implementation_plan.json`
|
|
54
|
+
const absolutePath = join(projectRoot, planRelPath)
|
|
55
|
+
|
|
56
|
+
let hash
|
|
57
|
+
try {
|
|
58
|
+
const body = readFileSync(absolutePath)
|
|
59
|
+
hash = createHash('sha256').update(body).digest('hex')
|
|
60
|
+
} catch (err) {
|
|
61
|
+
return { emitted: false, reason: `plan file unreadable: ${(err && err.message) || err}` }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const data = {
|
|
65
|
+
kind: 'plan',
|
|
66
|
+
path: planRelPath,
|
|
67
|
+
hash,
|
|
68
|
+
request_id: `plan-${taskId}-${Date.now()}`,
|
|
69
|
+
task_id: taskId,
|
|
70
|
+
terminal_id: terminalId,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const validation = validateConductorEvent('artifact.ready', data)
|
|
74
|
+
if (!validation.ok) {
|
|
75
|
+
return { emitted: false, reason: `schema violation: ${validation.error}` }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const envelope = {
|
|
79
|
+
t: new Date().toISOString(),
|
|
80
|
+
agent: `${hostname()}-${process.pid}`,
|
|
81
|
+
pid: process.pid,
|
|
82
|
+
hostname: hostname(),
|
|
83
|
+
incarnation: ++_incarnationCounter,
|
|
84
|
+
op: 'artifact.ready',
|
|
85
|
+
task: taskId,
|
|
86
|
+
data,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const eventsDir = join(projectRoot, '.aperant', 'events')
|
|
90
|
+
mkdirSync(eventsDir, { recursive: true })
|
|
91
|
+
const dayFile = `${new Date().toISOString().slice(0, 10)}.jsonl`
|
|
92
|
+
const fullPath = join(eventsDir, dayFile)
|
|
93
|
+
appendFileSync(fullPath, `${JSON.stringify(envelope)}\n`, 'utf-8')
|
|
94
|
+
|
|
95
|
+
return { emitted: true, envelope, file: dayFile }
|
|
96
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type declarations for the .mjs validator. Hand-authored because the
|
|
3
|
+
* import path points at src/.mjs directly (no dist roundtrip).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type ValidateResult = { ok: true } | { ok: false; error: string }
|
|
7
|
+
|
|
8
|
+
export function validateConductorEvent(
|
|
9
|
+
op: string,
|
|
10
|
+
data: Record<string, unknown> | undefined | null,
|
|
11
|
+
): ValidateResult
|
|
12
|
+
|
|
13
|
+
export const CONDUCTOR_VALIDATED_OPS: ReadonlySet<string>
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @internal
|
|
3
|
+
* event-schema.mjs — single source of truth for conductor event payload shapes.
|
|
4
|
+
*
|
|
5
|
+
* Why this lives in `framework`, not `core`:
|
|
6
|
+
* `packages/core` depends on `@aperant/framework`. The schema is shared by
|
|
7
|
+
* the CLI write path (`commands/event.mjs`), the framework lifecycle write
|
|
8
|
+
* seam (`coordination/store.mjs:appendEvent`), and the core IPC write seam
|
|
9
|
+
* (`packages/core/src/runtime/orchestration/conductor/event-log.ts`).
|
|
10
|
+
* Putting it in the lower layer keeps a single rule table that every
|
|
11
|
+
* writer can import.
|
|
12
|
+
*
|
|
13
|
+
* Shape: zero-dep plain JS validator returning a `{ ok, error? }` result.
|
|
14
|
+
* We deliberately avoid zod here so `.mjs` callers (event.mjs, store.mjs)
|
|
15
|
+
* don't need a build step, and so the core TS writer can use the same
|
|
16
|
+
* identity-shaped result without translating between zod and a custom
|
|
17
|
+
* error type.
|
|
18
|
+
*
|
|
19
|
+
* Per Codex roundtable lock 2026-05-15:
|
|
20
|
+
* - `phase.changed` is reserved for PTY busy/idle transitions ONLY.
|
|
21
|
+
* Plan completion uses `artifact.ready{kind:'plan'}`, not phase.changed.
|
|
22
|
+
* - `awaiting-input` / `input-rescinded` MUST carry both terminal_id and
|
|
23
|
+
* request_id so the drawer can pair cards strictly.
|
|
24
|
+
* - `artifact.ready{kind:'plan'}` is the canonical plan-completion signal
|
|
25
|
+
* consumed by the conductor's reviewer queue (Step 5's reviewPlan tool).
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/** Conductor ops the schema enforces. Other ops pass through unchanged. */
|
|
29
|
+
const CONDUCTOR_OPS = new Set([
|
|
30
|
+
'phase.changed',
|
|
31
|
+
'artifact.ready',
|
|
32
|
+
'awaiting-input',
|
|
33
|
+
'input-rescinded',
|
|
34
|
+
'heartbeat',
|
|
35
|
+
'budget.exceeded',
|
|
36
|
+
'terminal.enrolled',
|
|
37
|
+
'terminal.unenrolled',
|
|
38
|
+
'conductor.intent',
|
|
39
|
+
'conductor.cross_check',
|
|
40
|
+
'conductor.realignment',
|
|
41
|
+
// c-4 (action-capable) — gate verdict + boot-replay audit trail
|
|
42
|
+
'conductor.action.executed',
|
|
43
|
+
'conductor.action.gate_blocked',
|
|
44
|
+
'conductor.boot_reconciled',
|
|
45
|
+
])
|
|
46
|
+
|
|
47
|
+
/** Allowed values for `phase.changed.to` — PTY busy/idle only. */
|
|
48
|
+
const ALLOWED_PHASE_VALUES = new Set(['busy', 'idle'])
|
|
49
|
+
|
|
50
|
+
/** Allowed values for `artifact.ready.kind`. */
|
|
51
|
+
const ALLOWED_ARTIFACT_KINDS = new Set(['plan', 'spec', 'build', 'review'])
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Validate the data payload of a conductor event against the op's contract.
|
|
55
|
+
*
|
|
56
|
+
* @param {string} op
|
|
57
|
+
* @param {Record<string, unknown> | undefined | null} data
|
|
58
|
+
* @returns {{ ok: true } | { ok: false, error: string }}
|
|
59
|
+
*/
|
|
60
|
+
export function validateConductorEvent(op, data) {
|
|
61
|
+
if (!CONDUCTOR_OPS.has(op)) return { ok: true }
|
|
62
|
+
const d = data && typeof data === 'object' ? data : {}
|
|
63
|
+
|
|
64
|
+
switch (op) {
|
|
65
|
+
case 'awaiting-input':
|
|
66
|
+
case 'input-rescinded':
|
|
67
|
+
return requireFields(op, d, ['terminal_id', 'request_id'])
|
|
68
|
+
|
|
69
|
+
case 'phase.changed': {
|
|
70
|
+
const r = requireFields(op, d, ['from', 'to', 'terminal_id', 'request_id'])
|
|
71
|
+
if (!r.ok) return r
|
|
72
|
+
if (!ALLOWED_PHASE_VALUES.has(String(d.to))) {
|
|
73
|
+
return {
|
|
74
|
+
ok: false,
|
|
75
|
+
error: `phase.changed.to must be one of [${[...ALLOWED_PHASE_VALUES].join(', ')}], got: ${String(
|
|
76
|
+
d.to,
|
|
77
|
+
)}. Plan completion must use artifact.ready{kind:'plan'}.`,
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return { ok: true }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
case 'artifact.ready': {
|
|
84
|
+
const r = requireFields(op, d, [
|
|
85
|
+
'kind',
|
|
86
|
+
'path',
|
|
87
|
+
'hash',
|
|
88
|
+
'request_id',
|
|
89
|
+
'terminal_id',
|
|
90
|
+
'task_id',
|
|
91
|
+
])
|
|
92
|
+
if (!r.ok) return r
|
|
93
|
+
if (!ALLOWED_ARTIFACT_KINDS.has(String(d.kind))) {
|
|
94
|
+
return {
|
|
95
|
+
ok: false,
|
|
96
|
+
error: `artifact.ready.kind must be one of [${[...ALLOWED_ARTIFACT_KINDS].join(
|
|
97
|
+
', ',
|
|
98
|
+
)}], got: ${String(d.kind)}`,
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return { ok: true }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
case 'conductor.intent':
|
|
105
|
+
return requireFields(op, d, ['terminal_id', 'request_id', 'action', 'reasoning'])
|
|
106
|
+
|
|
107
|
+
case 'conductor.cross_check':
|
|
108
|
+
return requireFields(op, d, ['terminal_id', 'request_id'])
|
|
109
|
+
|
|
110
|
+
case 'conductor.realignment': {
|
|
111
|
+
const r = requireFields(op, d, ['terminal_id', 'request_id_orig'])
|
|
112
|
+
if (!r.ok) return r
|
|
113
|
+
if (d.attempt_n === undefined || d.attempt_n === null)
|
|
114
|
+
return { ok: false, error: `${op} missing required field(s): attempt_n` }
|
|
115
|
+
return { ok: true }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
case 'terminal.enrolled':
|
|
119
|
+
case 'terminal.unenrolled':
|
|
120
|
+
case 'heartbeat':
|
|
121
|
+
case 'budget.exceeded':
|
|
122
|
+
return requireFields(op, d, ['terminal_id'])
|
|
123
|
+
|
|
124
|
+
case 'conductor.action.executed':
|
|
125
|
+
return requireFields(op, d, ['terminal_id', 'request_id', 'action_kind', 'idempotency_key'])
|
|
126
|
+
|
|
127
|
+
case 'conductor.action.gate_blocked':
|
|
128
|
+
return requireFields(op, d, [
|
|
129
|
+
'terminal_id',
|
|
130
|
+
'request_id',
|
|
131
|
+
'action_kind',
|
|
132
|
+
'idempotency_key',
|
|
133
|
+
'blocked_reason',
|
|
134
|
+
])
|
|
135
|
+
|
|
136
|
+
case 'conductor.boot_reconciled': {
|
|
137
|
+
// Fleet-wide event — numeric fields only. All optional but if
|
|
138
|
+
// present must be finite numbers (zero allowed).
|
|
139
|
+
const numFields = ['cleaned_leases', 'replayed', 'start_offset']
|
|
140
|
+
for (const f of numFields) {
|
|
141
|
+
if (d[f] === undefined) continue
|
|
142
|
+
if (typeof d[f] !== 'number' || !Number.isFinite(d[f])) {
|
|
143
|
+
return {
|
|
144
|
+
ok: false,
|
|
145
|
+
error: `${op} field ${f} must be a finite number, got: ${String(d[f])}`,
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return { ok: true }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
default:
|
|
153
|
+
return { ok: true }
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Set of ops the schema validates — exported for test enumeration. */
|
|
158
|
+
export const CONDUCTOR_VALIDATED_OPS = CONDUCTOR_OPS
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* @param {string} op
|
|
162
|
+
* @param {Record<string, unknown>} d
|
|
163
|
+
* @param {string[]} fields
|
|
164
|
+
* @returns {{ ok: true } | { ok: false, error: string }}
|
|
165
|
+
*/
|
|
166
|
+
function requireFields(op, d, fields) {
|
|
167
|
+
const missing = fields.filter((f) => {
|
|
168
|
+
const v = d[f]
|
|
169
|
+
if (typeof v !== 'string' || v.length === 0) return true
|
|
170
|
+
return false
|
|
171
|
+
})
|
|
172
|
+
if (missing.length === 0) return { ok: true }
|
|
173
|
+
return { ok: false, error: `${op} missing required field(s): ${missing.join(', ')}` }
|
|
174
|
+
}
|
|
@@ -32,6 +32,7 @@ import { withFileLock } from '../util/fs-lock.mjs'
|
|
|
32
32
|
import { atomicWriteJson } from '../util/io.mjs'
|
|
33
33
|
import { getAgentIdentity, resolveSharedRoot } from './agent-identity.mjs'
|
|
34
34
|
import { appendLifecycleEvent } from './event-log.mjs'
|
|
35
|
+
import { validateConductorEvent } from './event-schema.mjs'
|
|
35
36
|
import {
|
|
36
37
|
listActive as teamStatusListActive,
|
|
37
38
|
removeSelf as teamStatusRemoveSelf,
|
|
@@ -128,9 +129,22 @@ export function createCoordinationStore(projectRoot) {
|
|
|
128
129
|
* @internal — R10 append-only event log. Emits one JSONL line per
|
|
129
130
|
* call via appendLifecycleEvent. Best-effort: filesystem errors are
|
|
130
131
|
* swallowed so a missing events/ dir never breaks task lifecycle.
|
|
132
|
+
*
|
|
133
|
+
* Step 1 — schema gate: conductor ops written via the store seam are
|
|
134
|
+
* validated against `event-schema.mjs` before they reach disk.
|
|
135
|
+
* Schema violations throw (caught here and surfaced via console.warn)
|
|
136
|
+
* because a malformed conductor event would silently break the
|
|
137
|
+
* drawer's pairing logic — we want it loud, not best-effort.
|
|
131
138
|
*/
|
|
132
139
|
appendEvent(evt) {
|
|
133
140
|
try {
|
|
141
|
+
if (evt && typeof evt.op === 'string') {
|
|
142
|
+
const validation = validateConductorEvent(evt.op, evt.data)
|
|
143
|
+
if (!validation.ok) {
|
|
144
|
+
console.warn(`[store.appendEvent] schema violation: ${validation.error}`)
|
|
145
|
+
return null
|
|
146
|
+
}
|
|
147
|
+
}
|
|
134
148
|
return appendLifecycleEvent(projectRoot, evt)
|
|
135
149
|
} catch {
|
|
136
150
|
/* best-effort — never block a lifecycle transition on the log */
|
package/src/cli/dispatch.mjs
CHANGED
|
@@ -71,6 +71,7 @@ import { cmdPrReview } from './commands/pr-review.mjs'
|
|
|
71
71
|
import { cmdPrecompactHook } from './commands/precompact-hook.mjs'
|
|
72
72
|
import { cmdProductContext } from './commands/product-context.mjs'
|
|
73
73
|
import { cmdProfile } from './commands/profile.mjs'
|
|
74
|
+
import { cmdReleaseNotes } from './commands/release-notes.mjs'
|
|
74
75
|
import { cmdRiskCheck } from './commands/risk-check.mjs'
|
|
75
76
|
import { cmdRoadmap } from './commands/roadmap.mjs'
|
|
76
77
|
import { cmdRoute } from './commands/route.mjs'
|
|
@@ -253,6 +254,7 @@ export const DISPATCH = {
|
|
|
253
254
|
'variant-pick': (args) => wrap(cmdVariantPick)(args[1], args[2], args.slice(3)),
|
|
254
255
|
worktree: (args) => wrap(cmdWorktree)(args[1], args[2], args.slice(3)),
|
|
255
256
|
audit: (args) => wrap(cmdAudit)(args[1], args[2], args.slice(3)),
|
|
257
|
+
'release-notes': (args) => wrap(cmdReleaseNotes)(args[1], args[2], args.slice(3)),
|
|
256
258
|
team: (args) => wrap(cmdTeam)(args[1], args[2], args.slice(3)),
|
|
257
259
|
active: (args) => wrap(cmdActive)(args[1], args[2], args.slice(3)),
|
|
258
260
|
roadmap: (args) => wrap(cmdRoadmap)(args[1], args[2], args.slice(3)),
|
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Skipped when {task_dir}/review.json is absent (review is optional for some
|
|
6
6
|
* tracks). When it is present, fails if any finding has
|
|
7
|
-
* severity ∈ {"critical", "warning"} AND resolved
|
|
7
|
+
* severity ∈ {"critical", "warning"} AND neither `resolved === true` nor
|
|
8
|
+
* `fixed === true`. The reviewer skill writes resolution as `fixed: true`
|
|
9
|
+
* paired with `fix_commit`; `resolved: true` is a legacy alias.
|
|
8
10
|
*
|
|
9
11
|
* The findings array may be under `findings` or `issues` depending on the
|
|
10
12
|
* reviewer format — we accept either.
|
|
@@ -47,7 +49,7 @@ const gate = {
|
|
|
47
49
|
? doc.issues
|
|
48
50
|
: []
|
|
49
51
|
const unresolved = findings.filter(
|
|
50
|
-
(f) => BLOCKING_SEVERITIES.has(f?.severity) && f?.resolved !== true,
|
|
52
|
+
(f) => BLOCKING_SEVERITIES.has(f?.severity) && f?.resolved !== true && f?.fixed !== true,
|
|
51
53
|
)
|
|
52
54
|
if (unresolved.length > 0) {
|
|
53
55
|
return {
|
package/src/cli/help.mjs
CHANGED
|
@@ -28,7 +28,7 @@ Commands:
|
|
|
28
28
|
task create <dir> --description <text> [--scope <s>] [--milestone <id>] [--phase <id>] [--roadmap-ref-url <url>] [--depends-on <refs>] [--track <T>] [--autonomy <N>] Create isolated task
|
|
29
29
|
task list <dir> [--scope <s>] [--blocked-by <id>] List active tasks (optionally filtered)
|
|
30
30
|
task get <dir> --id <task-id> Get task details and artifacts
|
|
31
|
-
task update <dir> --id <id> [--lifecycle-phase <p>] [--subtasks-total <N>] [--depends-on <refs>] Update non-linkage task state
|
|
31
|
+
task update <dir> --id <id> [--lifecycle-phase <p>] [--pr-url <url>] [--subtasks-total <N>] [--depends-on <refs>] Update non-linkage task state; --pr-url may arm ci-watch per ci_watch.after_ship
|
|
32
32
|
task move <dir> --id <id> [--to-scope <s>] [--to-milestone <id>] [--to-phase <id>] [--to-roadmap-ref-url <url>] [--reason <text>] [--force] Reassign scope/milestone/phase with history
|
|
33
33
|
task close <dir> --id <task-id> [--verdict <v>] Close task and move to completed
|
|
34
34
|
task narration-drain <dir> --task <task-id> Remove a row from state.pending_narration[] under withFileLock (called by /apt:close-task --narrate-only after a successful narrator spawn)
|
|
@@ -131,5 +131,5 @@ Flags:
|
|
|
131
131
|
|
|
132
132
|
All output is JSON.`
|
|
133
133
|
|
|
134
|
-
process.stdout.write(help
|
|
134
|
+
process.stdout.write(`${help}\n`)
|
|
135
135
|
}
|
|
@@ -60,6 +60,63 @@ export function readUpdateCache() {
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
/** Semver-shape pattern: MAJOR.MINOR.PATCH with optional pre-release and build metadata. */
|
|
64
|
+
const SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Read the worker's cache file with TTL and schema validation. Returns
|
|
68
|
+
* `{ fresh: true, cache }` when the cache exists, parses, declares
|
|
69
|
+
* `schema_version === 1`, and was written within `ttlMs` milliseconds.
|
|
70
|
+
* Returns `{ fresh: false }` when missing, stale, or schema mismatch.
|
|
71
|
+
*
|
|
72
|
+
* Security: `latest_version` and `installed_version` inside
|
|
73
|
+
* `cache.framework` are validated as semver strings. Invalid values are
|
|
74
|
+
* dropped (set to null) so they never propagate into shell-interpolated
|
|
75
|
+
* contexts.
|
|
76
|
+
*
|
|
77
|
+
* ENOENT and JSON parse errors stay silent (expected when the cache is
|
|
78
|
+
* missing or being rewritten). Unexpected errors are logged to stderr.
|
|
79
|
+
*
|
|
80
|
+
* @param {number} ttlMs Maximum age in milliseconds before cache is treated as stale.
|
|
81
|
+
* @returns {{ fresh: true, cache: Record<string, unknown> } | { fresh: false }}
|
|
82
|
+
*/
|
|
83
|
+
export function readFreshUpdateCache(ttlMs) {
|
|
84
|
+
const path = updateCachePath()
|
|
85
|
+
if (!existsSync(path)) return { fresh: false }
|
|
86
|
+
let data
|
|
87
|
+
try {
|
|
88
|
+
data = JSON.parse(readFileSync(path, 'utf-8'))
|
|
89
|
+
} catch (err) {
|
|
90
|
+
// SyntaxError = malformed JSON (expected); anything else is unexpected.
|
|
91
|
+
if (!(err instanceof SyntaxError)) {
|
|
92
|
+
console.error('[update-cache] read failed:', err.message)
|
|
93
|
+
}
|
|
94
|
+
return { fresh: false }
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
if (!data || typeof data !== 'object') return { fresh: false }
|
|
98
|
+
if (data.schema_version !== 1) return { fresh: false }
|
|
99
|
+
if (typeof data.checked_at !== 'string') return { fresh: false }
|
|
100
|
+
const age = Date.now() - new Date(data.checked_at).getTime()
|
|
101
|
+
if (!Number.isFinite(age) || age < 0 || age >= ttlMs) return { fresh: false }
|
|
102
|
+
// Validate semver fields in framework block — reject non-semver strings.
|
|
103
|
+
const fw = data.framework && typeof data.framework === 'object' ? data.framework : {}
|
|
104
|
+
const sanitized = { ...data, framework: { ...fw } }
|
|
105
|
+
for (const field of ['latest_version', 'installed_version']) {
|
|
106
|
+
if (
|
|
107
|
+
typeof sanitized.framework[field] === 'string' &&
|
|
108
|
+
!SEMVER_RE.test(sanitized.framework[field])
|
|
109
|
+
) {
|
|
110
|
+
sanitized.framework[field] = null
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return { fresh: true, cache: sanitized }
|
|
114
|
+
} catch (err) {
|
|
115
|
+
console.error('[update-cache] read failed:', err.message)
|
|
116
|
+
return { fresh: false }
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
63
120
|
/**
|
|
64
121
|
* Build the chip string array for a given cache. Empty array when there
|
|
65
122
|
* are no chips to show. Order: stale_files first (most urgent), then
|