@aperant/framework 0.6.3 → 0.6.4

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 (72) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/cli/artifacts/classification.d.mts.map +1 -1
  3. package/dist/cli/artifacts/classification.mjs +10 -0
  4. package/dist/cli/artifacts/classification.mjs.map +1 -1
  5. package/dist/cli/commands/init.d.mts.map +1 -1
  6. package/dist/cli/commands/init.mjs +73 -5
  7. package/dist/cli/commands/init.mjs.map +1 -1
  8. package/dist/cli/commands/pr-review-audit-fixer.d.mts +41 -2
  9. package/dist/cli/commands/pr-review-audit-fixer.d.mts.map +1 -1
  10. package/dist/cli/commands/pr-review-audit-fixer.mjs +91 -14
  11. package/dist/cli/commands/pr-review-audit-fixer.mjs.map +1 -1
  12. package/dist/cli/commands/route.d.mts.map +1 -1
  13. package/dist/cli/commands/route.mjs +10 -1
  14. package/dist/cli/commands/route.mjs.map +1 -1
  15. package/dist/cli/commands/task.d.mts.map +1 -1
  16. package/dist/cli/commands/task.mjs +28 -0
  17. package/dist/cli/commands/task.mjs.map +1 -1
  18. package/dist/cli/design/frontmatter-schema.d.mts +3 -3
  19. package/dist/cli/design/frontmatter-schema.d.mts.map +1 -1
  20. package/dist/cli/design/frontmatter-schema.mjs +3 -1
  21. package/dist/cli/design/frontmatter-schema.mjs.map +1 -1
  22. package/dist/cli/route/skill-discover.d.mts +2 -0
  23. package/dist/cli/route/skill-discover.d.mts.map +1 -1
  24. package/dist/cli/route/skill-discover.mjs +35 -1
  25. package/dist/cli/route/skill-discover.mjs.map +1 -1
  26. package/dist/cli/skill-author/contract.d.mts +19 -0
  27. package/dist/cli/skill-author/contract.d.mts.map +1 -1
  28. package/dist/cli/skill-author/contract.mjs +20 -0
  29. package/dist/cli/skill-author/contract.mjs.map +1 -1
  30. package/dist/cli/skill-author/skill-template.d.mts.map +1 -1
  31. package/dist/cli/skill-author/skill-template.mjs +4 -3
  32. package/dist/cli/skill-author/skill-template.mjs.map +1 -1
  33. package/package.json +5 -2
  34. package/skills/apt/SKILL.md +111 -5
  35. package/skills/apt-author-skill/SKILL.md +11 -0
  36. package/skills/apt-bootstrap/SKILL.md +1 -0
  37. package/skills/apt-classify/SKILL.md +1 -0
  38. package/skills/apt-close-task/SKILL.md +1 -0
  39. package/skills/apt-create-docs/SKILL.md +1 -0
  40. package/skills/apt-debug/SKILL.md +2 -0
  41. package/skills/apt-design/SKILL.md +2 -0
  42. package/skills/apt-discuss/SKILL.md +2 -0
  43. package/skills/apt-docs/SKILL.md +2 -0
  44. package/skills/apt-execute/SKILL.md +1 -0
  45. package/skills/apt-mockup/SKILL.md +2 -0
  46. package/skills/apt-pause/SKILL.md +1 -0
  47. package/skills/apt-personas/SKILL.md +1 -0
  48. package/skills/apt-plan/SKILL.md +2 -0
  49. package/skills/apt-pr-review/SKILL.md +1 -0
  50. package/skills/apt-quick/SKILL.md +2 -0
  51. package/skills/apt-resume/SKILL.md +1 -0
  52. package/skills/apt-review/SKILL.md +1 -0
  53. package/skills/apt-roadmap/SKILL.md +1 -0
  54. package/skills/apt-roundtable/SKILL.md +2 -0
  55. package/skills/apt-run/SKILL.md +1 -0
  56. package/skills/apt-scan/SKILL.md +1 -0
  57. package/skills/apt-setup/SKILL.md +1 -0
  58. package/skills/apt-ship/SKILL.md +6 -5
  59. package/skills/apt-stress-test/SKILL.md +1 -0
  60. package/skills/apt-terminal/SKILL.md +1 -0
  61. package/skills/apt-update/SKILL.md +3 -0
  62. package/skills/apt-verify/SKILL.md +1 -0
  63. package/skills/apt-verify-proof/SKILL.md +1 -0
  64. package/src/cli/artifacts/classification.mjs +10 -0
  65. package/src/cli/commands/init.mjs +83 -5
  66. package/src/cli/commands/pr-review-audit-fixer.mjs +95 -16
  67. package/src/cli/commands/route.mjs +10 -1
  68. package/src/cli/commands/task.mjs +27 -0
  69. package/src/cli/design/frontmatter-schema.mjs +3 -1
  70. package/src/cli/route/skill-discover.mjs +34 -1
  71. package/src/cli/skill-author/contract.mjs +22 -0
  72. package/src/cli/skill-author/skill-template.mjs +4 -3
@@ -117,7 +117,8 @@ function extractTopLevelSection(markdown, heading) {
117
117
  }
118
118
 
119
119
  /**
120
- * Parse a fixes-applied.md produced by apt-pr-review-fixer. Returns
120
+ * Parse a fixes-applied.md produced by apt-pr-review-fixer OR a
121
+ * self-review.md produced by apt-pr-review-self-reviewer. Returns
121
122
  * `{ fixes: Array<{id, title, file, verification}>, skipped: Array<{id}>, exists: true }`.
122
123
  * Non-existent or empty report returns `{ exists: false, fixes: [], skipped: [] }`.
123
124
  *
@@ -125,24 +126,57 @@ function extractTopLevelSection(markdown, heading) {
125
126
  * `### <id>` block. We strip any trailing `(created)` / `(modify)` etc
126
127
  * annotation the fixer template shows as free-form text so the intersect
127
128
  * step can match against `git diff --name-only` output cleanly.
129
+ *
130
+ * Dual-schema support (PR #95 Defect 2):
131
+ * - Fixer reports use `## Fixes Applied` with `### <ID> — title` headers
132
+ * (e.g. `### QUA-001 — title`). Real finding ids carry through.
133
+ * - Self-reviewer reports use `## Fixes Applied by Self-Reviewer` with
134
+ * `### Fix N: description` headers. These have no real finding id, so
135
+ * we synthesize `self-reviewer-fix-${N}` to satisfy the
136
+ * `report.fixes.length > 0` guard in decideVerdict rule 3 while
137
+ * letting the diff-intersect step (the real check) decide the verdict.
138
+ *
139
+ * The two heading texts are disjoint — fixers emit one, self-reviewers
140
+ * emit the other. If both somehow appear in the same file (not expected
141
+ * in practice), both are parsed and fixer entries come first.
128
142
  */
129
143
  export function parseFixesAppliedMarkdown(md) {
130
144
  if (!md || typeof md !== 'string' || !md.trim()) {
131
- return { exists: false, fixes: [], skipped: [] }
145
+ return { exists: false, fixes: [], skipped: [], schema: 'fixer' }
132
146
  }
133
147
  const fixesSection = extractTopLevelSection(md, 'Fixes Applied')
148
+ const selfReviewerSection = extractTopLevelSection(md, 'Fixes Applied by Self-Reviewer')
134
149
  const skippedSection = extractTopLevelSection(md, 'Skipped')
135
- const fixes = parseFixEntries(fixesSection)
150
+ const fixes = [
151
+ ...parseFixEntries(fixesSection),
152
+ ...parseSelfReviewerFixEntries(selfReviewerSection),
153
+ ]
136
154
  const skipped = parseSkippedEntries(skippedSection)
137
- return { exists: true, fixes, skipped }
155
+ // Detect which heading was present; fixer takes precedence when both appear.
156
+ const schema = fixesSection ? 'fixer' : 'self-reviewer'
157
+ return { exists: true, fixes, skipped, schema }
138
158
  }
139
159
 
140
- function parseFixEntries(section) {
160
+ /**
161
+ * Parse the body of a `## Fixes Applied by Self-Reviewer` section into
162
+ * entries shaped like fixer entries (id, title, file, files, verification).
163
+ *
164
+ * The self-reviewer agent emits `### Fix N: description` per fix instead
165
+ * of `### ID — title`, because the reviewer doesn't carry the original
166
+ * finding ids forward. We synthesize id `self-reviewer-fix-${N}` so the
167
+ * rest of the audit pipeline (decideVerdict rule 3 guard, audit.json
168
+ * envelope) stays schema-agnostic. The body shape — `- **File**:` /
169
+ * `- **Change**:` — is identical to fixer entries, so we reuse
170
+ * extractFileFields verbatim. Verification is not part of the self-
171
+ * reviewer schema (their PASS/FAIL signal lives in `## Verification
172
+ * Results` and is already covered by parseFixerStatusLine), so we leave
173
+ * `verification: null` and let the gate's diff-intersect step decide.
174
+ */
175
+ function parseSelfReviewerFixEntries(section) {
141
176
  if (!section) return []
142
- // `### <id> <title>` OR `### <id> -- <title>` OR `### <id>` alone.
143
- // The fixer template uses em-dash + space; we tolerate either.
177
+ // `### Fix 1: description` / `### Fix 2: description`.
144
178
  const entries = []
145
- const headerRx = /^###\s+([A-Z]+-\d+)(?:\s*[—-]{1,2}\s*(.+))?$/gm
179
+ const headerRx = /^###\s+Fix\s+(\d+)(?:\s*:\s*(.+))?$/gm
146
180
  const matches = [...section.matchAll(headerRx)]
147
181
  for (let i = 0; i < matches.length; i++) {
148
182
  const m = matches[i]
@@ -151,10 +185,8 @@ function parseFixEntries(section) {
151
185
  const body = section.slice(start, nextStart)
152
186
  const files = extractFileFields(body)
153
187
  entries.push({
154
- id: m[1],
188
+ id: `self-reviewer-fix-${m[1]}`,
155
189
  title: m[2] ? m[2].trim() : null,
156
- // Back-compat: single-path consumers + tests still see `file`
157
- // (the first extracted path). New code should prefer `files`.
158
190
  file: files[0] ?? null,
159
191
  files,
160
192
  verification: extractVerificationField(body),
@@ -163,6 +195,48 @@ function parseFixEntries(section) {
163
195
  return entries
164
196
  }
165
197
 
198
+ function parseFixEntries(section) {
199
+ if (!section) return []
200
+ // `### <id> — <title>` OR `### <id> -- <title>` OR `### <id>` alone.
201
+ // The fixer template uses em-dash + space; we tolerate either.
202
+ //
203
+ // PR #95 (2026-05-12) widened the capture to accept combined-id headers
204
+ // like `### QUA-003 + FIT-001 — title` where one body explains multiple
205
+ // findings. The locked separator alphabet is `+`, `,`, and ` and ` —
206
+ // the exact set observed in live fixer reports. We do NOT widen to a
207
+ // generic `\W+` splitter; the alphabet stays narrow so a typo like
208
+ // `### QUA-003-foo` cannot be mis-parsed as id `QUA-003`. Combined
209
+ // headers produce one entry per id, all sharing the same body fields
210
+ // (files, verification, title).
211
+ const entries = []
212
+ const headerRx =
213
+ /^###\s+([A-Z]+-\d+(?:\s*(?:\+|,|\s+and\s+)\s*[A-Z]+-\d+)*)(?:\s*[—-]{1,2}\s*(.+))?$/gm
214
+ const idSplitRx = /\s*(?:\+|,|\s+and\s+)\s*/
215
+ const matches = [...section.matchAll(headerRx)]
216
+ for (let i = 0; i < matches.length; i++) {
217
+ const m = matches[i]
218
+ const start = m.index
219
+ const nextStart = i + 1 < matches.length ? matches[i + 1].index : section.length
220
+ const body = section.slice(start, nextStart)
221
+ const files = extractFileFields(body)
222
+ const title = m[2] ? m[2].trim() : null
223
+ const verification = extractVerificationField(body)
224
+ const ids = m[1].split(idSplitRx)
225
+ for (const id of ids) {
226
+ entries.push({
227
+ id,
228
+ title,
229
+ // Back-compat: single-path consumers + tests still see `file`
230
+ // (the first extracted path). New code should prefer `files`.
231
+ file: files[0] ?? null,
232
+ files,
233
+ verification,
234
+ })
235
+ }
236
+ }
237
+ return entries
238
+ }
239
+
166
240
  function parseSkippedEntries(section) {
167
241
  if (!section) return []
168
242
  const entries = []
@@ -518,12 +592,14 @@ export function decideVerdict({ statusLine, fixesApplied, diffFiles }) {
518
592
  // fail-closed trap. The report could be scaffolded with headers but
519
593
  // no actual entries.
520
594
  if (claimed.fixed > 0 && report.fixes.length === 0) {
595
+ const heading =
596
+ report.schema === 'self-reviewer' ? '## Fixes Applied by Self-Reviewer' : '## Fixes Applied'
521
597
  return {
522
598
  verdict: 'hallucinated',
523
599
  reason:
524
600
  `fixer claimed FIXED=${claimed.fixed} but fixes-applied.md has zero ` +
525
601
  `### <id>` +
526
- ` entries under ## Fixes Applied`,
602
+ ` entries under ${heading}`,
527
603
  claimed,
528
604
  matched: [],
529
605
  unmatched: [],
@@ -551,10 +627,13 @@ export function decideVerdict({ statusLine, fixesApplied, diffFiles }) {
551
627
  // Main rule: FIXED > 0 AND PASS — intersect claimed files with the
552
628
  // worktree diff. Empty intersection = hallucination.
553
629
  if (claimed.fixed > 0 && claimed.verification === 'PASS') {
554
- const claimedFiles = report.fixes.flatMap((f) =>
555
- Array.isArray(f.files) ? f.files : f.file ? [f.file] : [],
556
- )
557
- const { matched, unmatched } = intersectClaimedAndStaged(claimedFiles, diffFiles)
630
+ const claimedFiles = [
631
+ ...new Set(
632
+ report.fixes.flatMap((f) => (Array.isArray(f.files) ? f.files : f.file ? [f.file] : [])),
633
+ ),
634
+ ]
635
+ const { matched: rawMatched, unmatched } = intersectClaimedAndStaged(claimedFiles, diffFiles)
636
+ const matched = [...new Set(rawMatched)]
558
637
  if (matched.length === 0) {
559
638
  return {
560
639
  verdict: 'hallucinated',
@@ -138,7 +138,7 @@ const PR_REVIEW_SIGNAL = /(?:\bPR\s*#?\s*\d+|#\d+|\bpull\s*request\s*#?\s*\d+)\b
138
138
  * so this keeps the two paths consistent.
139
139
  *
140
140
  * @param {import('../route/skill-discover.mjs').DiscoveredSkill[]} skills
141
- * @returns {{ agentOf: (slug: string) => string|null, spawnsAgent: (slug: string) => boolean, knownSlugs: Set<string>, isUserInvocable: (slug: string) => boolean }}
141
+ * @returns {{ agentOf: (slug: string) => string|null, spawnsAgent: (slug: string) => boolean, taskContextOf: (slug: string) => string|null, defaultTrackOf: (slug: string) => string|null, knownSlugs: Set<string>, isUserInvocable: (slug: string) => boolean }}
142
142
  */
143
143
  function skillIndex(skills) {
144
144
  const byName = new Map()
@@ -160,6 +160,12 @@ function skillIndex(skills) {
160
160
  spawnsAgent(slug) {
161
161
  return Boolean(byName.get(slug)?.spawns_agent)
162
162
  },
163
+ taskContextOf(slug) {
164
+ return byName.get(slug)?.task_context ?? null
165
+ },
166
+ defaultTrackOf(slug) {
167
+ return byName.get(slug)?.default_track ?? null
168
+ },
163
169
  isUserInvocable(slug) {
164
170
  return userInvocableSlugs.has(slug)
165
171
  },
@@ -322,6 +328,7 @@ export function cmdRoute(projectDir, extraArgs) {
322
328
  if (!isPrReviewFreetext && index.knownSlugs.has(firstWord)) {
323
329
  const shouldSpawn = index.spawnsAgent(firstWord)
324
330
  const gate_preview = previewGatesFor(firstWord)
331
+ const defaultTrack = index.defaultTrackOf(firstWord)
325
332
  return ok({
326
333
  status: 'ok',
327
334
  command: 'route',
@@ -330,6 +337,8 @@ export function cmdRoute(projectDir, extraArgs) {
330
337
  skill_args: remainingArgs || null,
331
338
  spawn_agent: shouldSpawn,
332
339
  agent: shouldSpawn ? index.agentOf(firstWord) || `apt-${firstWord}` : null,
340
+ task_context: index.taskContextOf(firstWord),
341
+ ...(defaultTrack ? { default_track: defaultTrack } : {}),
333
342
  host_capabilities,
334
343
  task_isolation,
335
344
  update_check,
@@ -779,6 +779,13 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
779
779
  // C56 A2 — `--pr-url <url>` records the GitHub PR URL opened by
780
780
  // apt:ship. Downstream consumers: task close (auto_close_phase
781
781
  // gate) and `task close-merged` (/apt:close-task).
782
+ //
783
+ // Defect fix (12-05-26): when the caller does NOT also pass
784
+ // `--lifecycle-phase` and the task is currently in `reviewing`,
785
+ // auto-flip lifecycle to `shipped-pending-merge` atomically.
786
+ // This collapses the two-call apt:ship contract into a single
787
+ // load-bearing call so orchestrators can't drop the lifecycle
788
+ // transition. Idempotent / no-op on any other phase.
782
789
  if (flags.has('pr-url')) {
783
790
  const url = flags.get('pr-url')
784
791
  if (!url || typeof url !== 'string') {
@@ -794,6 +801,26 @@ export function cmdTask(subcommand, projectDir, extraArgs) {
794
801
  return
795
802
  }
796
803
  task.pr_url = url
804
+ if (!flags.has('lifecycle-phase') && task.lifecycle_phase === 'reviewing') {
805
+ task.lifecycle_phase = 'shipped-pending-merge'
806
+ if (!Array.isArray(task.lifecycle_history)) task.lifecycle_history = []
807
+ task.lifecycle_history.push({
808
+ phase: 'shipped-pending-merge',
809
+ at: new Date().toISOString(),
810
+ reason: 'auto-flip on --pr-url',
811
+ })
812
+ store.appendEvent({
813
+ op: 'task.lifecycle.shipped-pending-merge',
814
+ task: taskId,
815
+ data: {
816
+ lifecycle_phase: 'shipped-pending-merge',
817
+ scope: task.scope || 'project',
818
+ milestone_id: task.milestone_id ?? null,
819
+ phase_id: task.phase_id ?? null,
820
+ reason: 'auto-flip on --pr-url',
821
+ },
822
+ })
823
+ }
797
824
  }
798
825
  if (flags.has('subtasks-total'))
799
826
  task.progress.subtasks_total = parseInt(flags.get('subtasks-total'), 10)
@@ -83,7 +83,9 @@ export const TypographySchema = z
83
83
  .object({
84
84
  families: TypographyFamiliesSchema,
85
85
  scale: z.array(PixelOrRem).min(3), // at least {body, heading, display}
86
- weights: z.array(z.number().int().positive()).optional(),
86
+ weights: z
87
+ .union([z.array(z.number().int().positive()), z.record(z.string(), z.number().positive())])
88
+ .optional(),
87
89
  lineHeight: z.record(z.string(), NonEmptyString).optional(),
88
90
  })
89
91
  .passthrough()
@@ -47,6 +47,8 @@ const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---\n/
47
47
  * @property {string[]} gates
48
48
  * @property {string} default_execution_mode
49
49
  * @property {string[]} execution_modes
50
+ * @property {'create-new'|'require-existing'|'self-managed'|'none'} task_context
51
+ * @property {('QUICK'|'STANDARD'|'DEEP'|'DEBUG')|undefined} default_track
50
52
  * @property {string} file_path absolute path to the SKILL.md
51
53
  */
52
54
 
@@ -165,6 +167,8 @@ function readSkill(file) {
165
167
  gates: Array.isArray(d.gates) ? [...d.gates] : [],
166
168
  default_execution_mode: d.default_execution_mode,
167
169
  execution_modes: Array.isArray(d.execution_modes) ? [...d.execution_modes] : [],
170
+ task_context: d.task_context,
171
+ default_track: d.default_track,
168
172
  file_path: file,
169
173
  },
170
174
  }
@@ -206,11 +210,38 @@ function walkSkillRoot(root) {
206
210
  return out
207
211
  }
208
212
 
213
+ /**
214
+ * Redact raw attacker-controlled values from Zod validation error strings
215
+ * before they are written to the log (SEC-002). Zod enum rejection messages
216
+ * embed the received value verbatim: "Invalid enum value. Expected ...,
217
+ * received 'evil-payload'". We strip the received portion and replace it
218
+ * with a byte-length indicator so downstream log consumers never see the
219
+ * raw attacker string.
220
+ *
221
+ * @param {string[]} errors
222
+ * @returns {string[]}
223
+ */
224
+ function redactErrors(errors) {
225
+ if (!Array.isArray(errors)) return errors
226
+ return errors.map((msg) => {
227
+ if (typeof msg !== 'string') return msg
228
+ // Replace ", received '<anything>'" with a length annotation.
229
+ // The regex is anchored to the literal Zod enum error suffix so
230
+ // only the received-value fragment is removed, not diagnostic context.
231
+ return msg.replace(/, received '([^']*)'/g, (_, v) => `, received [${v.length} chars redacted]`)
232
+ })
233
+ }
234
+
209
235
  /**
210
236
  * Append one dropped record to .aperant/logs/route-dropped.jsonl. Best-
211
237
  * effort — if the log dir can't be created we swallow the error (the
212
238
  * router still returns the envelope without the skill).
213
239
  *
240
+ * SEC-002: the `errors` array may contain raw attacker-controlled field
241
+ * values from Zod enum rejections. redactErrors() strips the received-value
242
+ * fragment before the record is serialized so log consumers (dashboards,
243
+ * alert rules) never see the raw attacker string.
244
+ *
214
245
  * @param {string} targetDir
215
246
  * @param {DiscoveredDrop} drop
216
247
  */
@@ -218,9 +249,11 @@ function logDropped(targetDir, drop) {
218
249
  try {
219
250
  const logDir = join(targetDir, '.aperant', 'logs')
220
251
  mkdirSync(logDir, { recursive: true })
252
+ const safeErrors = drop.errors ? redactErrors(drop.errors) : undefined
253
+ const record = safeErrors !== undefined ? { ...drop, errors: safeErrors } : drop
221
254
  appendFileSync(
222
255
  join(logDir, 'route-dropped.jsonl'),
223
- `${JSON.stringify({ ts: new Date().toISOString(), ...drop })}\n`,
256
+ `${JSON.stringify({ ts: new Date().toISOString(), ...record })}\n`,
224
257
  'utf-8',
225
258
  )
226
259
  } catch {
@@ -80,6 +80,26 @@ export const STAGES = Object.freeze([
80
80
  */
81
81
  export const EXECUTION_MODES = Object.freeze(['auto', 'step', 'plan-mode', 'plan-only', 'research'])
82
82
 
83
+ /**
84
+ * Skill-passthrough policy values (router-0.4 / G25 fix). The `apt/SKILL.md`
85
+ * orchestrator dispatches on this frontmatter field — missing values fail
86
+ * closed so new skills can't silently bypass task registration. See
87
+ * docs/frameworks/spec-gaps.md#g25 for the defect that motivated this.
88
+ */
89
+ export const TASK_CONTEXTS = Object.freeze([
90
+ 'create-new',
91
+ 'require-existing',
92
+ 'self-managed',
93
+ 'none',
94
+ ])
95
+
96
+ /**
97
+ * Optional default track for skills with `task_context: create-new`. The
98
+ * router falls back to a per-slug hardcoded table when this field is omitted;
99
+ * declaring it here lets a skill own its default without a router code change.
100
+ */
101
+ export const TRACK_VALUES = Object.freeze(['QUICK', 'STANDARD', 'DEEP', 'DEBUG'])
102
+
83
103
  /**
84
104
  * Required XML-style section tags that MUST appear (opening + closing) in the
85
105
  * SKILL.md body. Order is not enforced — authors can lay out the sections
@@ -138,6 +158,8 @@ export const SkillFrontmatterSchema = z
138
158
  gates: z.array(z.string()),
139
159
  default_execution_mode: ExecutionModeSchema,
140
160
  execution_modes: z.array(ExecutionModeSchema).min(1),
161
+ task_context: z.enum([...TASK_CONTEXTS]),
162
+ default_track: z.enum([...TRACK_VALUES]).optional(),
141
163
  // Legacy / optional pass-throughs — kept as opt-in so migrations
142
164
  // don't immediately break skills that carry them.
143
165
  triggers: z.array(z.string()).optional(),
@@ -25,13 +25,14 @@ user_invocable: true
25
25
  internal: false
26
26
  spawns_agent: false
27
27
  agent_name: null
28
- allowed-tools: "Read, Grep, Glob"
29
- argument-hint: "${fullName} [args]"
30
- gates: []
28
+ task_context: create-new # one of: create-new | require-existing | self-managed | none — see packages/framework/docs/skill-passthrough.md
31
29
  default_execution_mode: auto
32
30
  execution_modes:
33
31
  - auto
34
32
  - step
33
+ allowed-tools: "Read, Grep, Glob"
34
+ argument-hint: "${fullName} [args]"
35
+ gates: []
35
36
  triggers:
36
37
  - /${fullName}
37
38
  - ${fullName}