@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.
- package/CHANGELOG.md +23 -0
- package/dist/cli/artifacts/classification.d.mts.map +1 -1
- package/dist/cli/artifacts/classification.mjs +10 -0
- package/dist/cli/artifacts/classification.mjs.map +1 -1
- package/dist/cli/commands/init.d.mts.map +1 -1
- package/dist/cli/commands/init.mjs +73 -5
- package/dist/cli/commands/init.mjs.map +1 -1
- package/dist/cli/commands/pr-review-audit-fixer.d.mts +41 -2
- package/dist/cli/commands/pr-review-audit-fixer.d.mts.map +1 -1
- package/dist/cli/commands/pr-review-audit-fixer.mjs +91 -14
- package/dist/cli/commands/pr-review-audit-fixer.mjs.map +1 -1
- package/dist/cli/commands/route.d.mts.map +1 -1
- package/dist/cli/commands/route.mjs +10 -1
- package/dist/cli/commands/route.mjs.map +1 -1
- package/dist/cli/commands/task.d.mts.map +1 -1
- package/dist/cli/commands/task.mjs +28 -0
- package/dist/cli/commands/task.mjs.map +1 -1
- package/dist/cli/design/frontmatter-schema.d.mts +3 -3
- package/dist/cli/design/frontmatter-schema.d.mts.map +1 -1
- package/dist/cli/design/frontmatter-schema.mjs +3 -1
- package/dist/cli/design/frontmatter-schema.mjs.map +1 -1
- package/dist/cli/route/skill-discover.d.mts +2 -0
- package/dist/cli/route/skill-discover.d.mts.map +1 -1
- package/dist/cli/route/skill-discover.mjs +35 -1
- package/dist/cli/route/skill-discover.mjs.map +1 -1
- package/dist/cli/skill-author/contract.d.mts +19 -0
- package/dist/cli/skill-author/contract.d.mts.map +1 -1
- package/dist/cli/skill-author/contract.mjs +20 -0
- package/dist/cli/skill-author/contract.mjs.map +1 -1
- package/dist/cli/skill-author/skill-template.d.mts.map +1 -1
- package/dist/cli/skill-author/skill-template.mjs +4 -3
- package/dist/cli/skill-author/skill-template.mjs.map +1 -1
- package/package.json +5 -2
- package/skills/apt/SKILL.md +111 -5
- package/skills/apt-author-skill/SKILL.md +11 -0
- package/skills/apt-bootstrap/SKILL.md +1 -0
- package/skills/apt-classify/SKILL.md +1 -0
- package/skills/apt-close-task/SKILL.md +1 -0
- package/skills/apt-create-docs/SKILL.md +1 -0
- package/skills/apt-debug/SKILL.md +2 -0
- package/skills/apt-design/SKILL.md +2 -0
- package/skills/apt-discuss/SKILL.md +2 -0
- package/skills/apt-docs/SKILL.md +2 -0
- package/skills/apt-execute/SKILL.md +1 -0
- package/skills/apt-mockup/SKILL.md +2 -0
- package/skills/apt-pause/SKILL.md +1 -0
- package/skills/apt-personas/SKILL.md +1 -0
- package/skills/apt-plan/SKILL.md +2 -0
- package/skills/apt-pr-review/SKILL.md +1 -0
- package/skills/apt-quick/SKILL.md +2 -0
- package/skills/apt-resume/SKILL.md +1 -0
- package/skills/apt-review/SKILL.md +1 -0
- package/skills/apt-roadmap/SKILL.md +1 -0
- package/skills/apt-roundtable/SKILL.md +2 -0
- package/skills/apt-run/SKILL.md +1 -0
- package/skills/apt-scan/SKILL.md +1 -0
- package/skills/apt-setup/SKILL.md +1 -0
- package/skills/apt-ship/SKILL.md +6 -5
- package/skills/apt-stress-test/SKILL.md +1 -0
- package/skills/apt-terminal/SKILL.md +1 -0
- package/skills/apt-update/SKILL.md +3 -0
- package/skills/apt-verify/SKILL.md +1 -0
- package/skills/apt-verify-proof/SKILL.md +1 -0
- package/src/cli/artifacts/classification.mjs +10 -0
- package/src/cli/commands/init.mjs +83 -5
- package/src/cli/commands/pr-review-audit-fixer.mjs +95 -16
- package/src/cli/commands/route.mjs +10 -1
- package/src/cli/commands/task.mjs +27 -0
- package/src/cli/design/frontmatter-schema.mjs +3 -1
- package/src/cli/route/skill-discover.mjs +34 -1
- package/src/cli/skill-author/contract.mjs +22 -0
- 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
|
|
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 =
|
|
150
|
+
const fixes = [
|
|
151
|
+
...parseFixEntries(fixesSection),
|
|
152
|
+
...parseSelfReviewerFixEntries(selfReviewerSection),
|
|
153
|
+
]
|
|
136
154
|
const skipped = parseSkippedEntries(skippedSection)
|
|
137
|
-
|
|
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
|
-
|
|
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
|
-
// `###
|
|
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+(
|
|
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
|
|
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 =
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
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
|
|
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(), ...
|
|
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
|
-
|
|
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}
|