@aperant/framework 0.6.5 → 0.6.6

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.
Files changed (64) hide show
  1. package/README.md +17 -251
  2. package/dist/cli/commands/commit.d.mts.map +1 -1
  3. package/dist/cli/commands/commit.mjs +25 -8
  4. package/dist/cli/commands/commit.mjs.map +1 -1
  5. package/dist/cli/commands/event.d.mts.map +1 -1
  6. package/dist/cli/commands/event.mjs +66 -0
  7. package/dist/cli/commands/event.mjs.map +1 -1
  8. package/dist/cli/commands/modes.d.mts.map +1 -1
  9. package/dist/cli/commands/modes.mjs +2 -1
  10. package/dist/cli/commands/modes.mjs.map +1 -1
  11. package/dist/cli/commands/task.d.mts.map +1 -1
  12. package/dist/cli/commands/task.mjs +285 -139
  13. package/dist/cli/commands/task.mjs.map +1 -1
  14. package/dist/cli/commands/triage.d.mts.map +1 -1
  15. package/dist/cli/commands/triage.mjs +9 -5
  16. package/dist/cli/commands/triage.mjs.map +1 -1
  17. package/dist/cli/coordination/auto-emit-artifact-ready.d.mts +16 -0
  18. package/dist/cli/coordination/auto-emit-artifact-ready.d.mts.map +1 -0
  19. package/dist/cli/coordination/auto-emit-artifact-ready.mjs +88 -0
  20. package/dist/cli/coordination/auto-emit-artifact-ready.mjs.map +1 -0
  21. package/dist/cli/coordination/event-schema.d.mts +16 -0
  22. package/dist/cli/coordination/event-schema.d.mts.map +1 -0
  23. package/dist/cli/coordination/event-schema.mjs +161 -0
  24. package/dist/cli/coordination/event-schema.mjs.map +1 -0
  25. package/dist/cli/coordination/store.d.mts +6 -0
  26. package/dist/cli/coordination/store.d.mts.map +1 -1
  27. package/dist/cli/coordination/store.mjs +14 -0
  28. package/dist/cli/coordination/store.mjs.map +1 -1
  29. package/dist/cli/help.mjs +2 -2
  30. package/dist/cli/help.mjs.map +1 -1
  31. package/dist/cli/roadmap/conductor-view.d.mts +13 -0
  32. package/dist/cli/roadmap/conductor-view.d.mts.map +1 -0
  33. package/dist/cli/roadmap/conductor-view.mjs +31 -0
  34. package/dist/cli/roadmap/conductor-view.mjs.map +1 -0
  35. package/dist/cli/route/skill-discover.d.mts.map +1 -1
  36. package/dist/cli/route/skill-discover.mjs +2 -1
  37. package/dist/cli/route/skill-discover.mjs.map +1 -1
  38. package/dist/cli/task/ids.d.mts +7 -4
  39. package/dist/cli/task/ids.d.mts.map +1 -1
  40. package/dist/cli/task/ids.mjs +11 -10
  41. package/dist/cli/task/ids.mjs.map +1 -1
  42. package/package.json +133 -125
  43. package/prompts/conductor-status-check.md +23 -0
  44. package/prompts/conductor-sub-agent.md +57 -0
  45. package/prompts/conductor-system.md +172 -0
  46. package/skills/apt-plan/SKILL.md +12 -0
  47. package/skills/apt-plan/adapters/conductor.md +98 -0
  48. package/skills/apt-setup/SKILL.md +2 -0
  49. package/skills/apt-ship/SKILL.md +27 -12
  50. package/skills/apt-spar/SKILL.md +290 -0
  51. package/src/cli/commands/commit.mjs +27 -8
  52. package/src/cli/commands/event.mjs +68 -0
  53. package/src/cli/commands/modes.mjs +2 -1
  54. package/src/cli/commands/task.mjs +305 -152
  55. package/src/cli/commands/triage.mjs +14 -5
  56. package/src/cli/coordination/auto-emit-artifact-ready.mjs +96 -0
  57. package/src/cli/coordination/event-schema.d.ts +13 -0
  58. package/src/cli/coordination/event-schema.mjs +174 -0
  59. package/src/cli/coordination/store.mjs +14 -0
  60. package/src/cli/help.mjs +2 -2
  61. package/src/cli/roadmap/conductor-view.d.ts +10 -0
  62. package/src/cli/roadmap/conductor-view.mjs +31 -0
  63. package/src/cli/route/skill-discover.mjs +2 -1
  64. package/src/cli/task/ids.mjs +15 -13
@@ -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/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 + '\n')
134
+ process.stdout.write(`${help}\n`)
135
135
  }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Type declarations for the .mjs conductor-view helper.
3
+ * Hand-authored — the import path points at src/.mjs directly.
4
+ */
5
+
6
+ export interface ConductorRoadmapView {
7
+ scopes: Array<{ scope: string; roadmap: unknown }>
8
+ }
9
+
10
+ export function buildConductorRoadmapView(projectRoot: string): ConductorRoadmapView
@@ -0,0 +1,31 @@
1
+ /**
2
+ * conductor-view.mjs — read-only roadmap surface for the Aperant
3
+ * Conductor's `getRoadmap` tool.
4
+ *
5
+ * Wraps `loadRoadmapForScope` + `listRoadmapScopes` so the Conductor sees
6
+ * the same overlay-aware view every framework command sees. Direct reads
7
+ * of `.aperant/roadmap/*\/phases/*.json` or `milestones.json` would miss
8
+ * the dual-write overlay (seed_id from legacy blob, etc.) — Codex
9
+ * roundtable correction #3.
10
+ *
11
+ * Step 5 of Conductor v2 ship order.
12
+ */
13
+
14
+ import { loadRoadmapForScope } from './io.mjs'
15
+ import { listRoadmapScopes } from './paths.mjs'
16
+
17
+ /**
18
+ * Build a flat per-scope roadmap snapshot for the Conductor.
19
+ *
20
+ * @param {string} projectRoot
21
+ * @returns {{ scopes: Array<{ scope: string, roadmap: unknown }> }}
22
+ */
23
+ export function buildConductorRoadmapView(projectRoot) {
24
+ const scopes = listRoadmapScopes(projectRoot)
25
+ const out = []
26
+ for (const scope of scopes) {
27
+ const roadmap = loadRoadmapForScope(projectRoot, scope)
28
+ if (roadmap !== null) out.push({ scope, roadmap })
29
+ }
30
+ return { scopes: out }
31
+ }
@@ -29,6 +29,7 @@ import {
29
29
  } from 'node:fs'
30
30
  import { isAbsolute, join, relative, resolve } from 'node:path'
31
31
  import { parse as parseYaml } from 'yaml'
32
+ import { sanitizeAptVersionForYamlParse } from '../install/version-header.mjs'
32
33
  import { SkillFrontmatterSchema } from '../skill-author/contract.mjs'
33
34
 
34
35
  const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---\n/
@@ -126,7 +127,7 @@ function readSkill(file) {
126
127
  }
127
128
  let parsed
128
129
  try {
129
- parsed = parseYaml(m[1])
130
+ parsed = parseYaml(sanitizeAptVersionForYamlParse(m[1]))
130
131
  } catch (err) {
131
132
  return {
132
133
  ok: false,
@@ -9,8 +9,7 @@ import { join, resolve } from 'node:path'
9
9
  * Supported formats: YY-MM-DD (default, sorts chronologically), DD-MM-YY, MM-DD-YY
10
10
  * Uses hyphens as separators for filesystem safety.
11
11
  */
12
- export function formatDateForTask(dateFormat) {
13
- const now = new Date()
12
+ export function formatDateForTask(dateFormat, now = new Date()) {
14
13
  const yy = String(now.getFullYear()).slice(2)
15
14
  const mm = String(now.getMonth() + 1).padStart(2, '0')
16
15
  const dd = String(now.getDate()).padStart(2, '0')
@@ -27,20 +26,23 @@ export function formatDateForTask(dateFormat) {
27
26
 
28
27
  /**
29
28
  * Generate a short task ID from description + timestamp.
30
- * Format: {date}_{slug}-{4-char-hash}
31
- * Date comes first so tasks sort chronologically in file explorers.
29
+ * Format: {slug}_{date}
30
+ * Slug comes first so task lists scan by verb instead of by date.
32
31
  * Date format is configurable via preferences.date_format in config.
32
+ * Same-day same-slug collisions are disambiguated by the caller
33
+ * (see commands/task.mjs) via a numeric -2, -3, … suffix; this
34
+ * generator never emits a counter or a random hash.
33
35
  */
34
- export function generateTaskId(description, config) {
35
- const slug = description
36
- .toLowerCase()
37
- .replace(/[^a-z0-9]+/g, '-')
38
- .replace(/^-|-$/g, '')
39
- .slice(0, 40)
36
+ export function generateTaskId(description, config, now = new Date()) {
37
+ const slug =
38
+ description
39
+ .toLowerCase()
40
+ .replace(/[^a-z0-9]+/g, '-')
41
+ .slice(0, 40)
42
+ .replace(/^-|-$/g, '') || 'task'
40
43
  const dateFormat = config?.preferences?.date_format || 'YY-MM-DD'
41
- const date = formatDateForTask(dateFormat)
42
- const hash = Math.random().toString(36).slice(2, 6)
43
- return `${date}_${slug}-${hash}`
44
+ const date = formatDateForTask(dateFormat, now)
45
+ return `${slug}_${date}`
44
46
  }
45
47
 
46
48
  export function getTaskDir(projectDir, taskId) {