@godv61/dsh-task-engine 0.26.1 → 0.27.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.acceptance.mjs +11 -6
- package/.assessment-batch1.mjs +14 -11
- package/.codex-project-test.mjs +80 -0
- package/.enforce-test.mjs +12 -17
- package/.evidence-test.mjs +8 -8
- package/.filter-test.mjs +6 -8
- package/.freeze-test.mjs +44 -40
- package/.p0-test.mjs +21 -24
- package/.revision-test.mjs +16 -16
- package/.roundtrip-test.mjs +247 -120
- package/.workflow-test.mjs +119 -102
- package/README.md +14 -12
- package/defaults/eng.json +1 -97
- package/docs/CHANGELOG.md +33 -19
- package/docs/configuration.md +22 -21
- package/docs/development.md +1 -1
- package/docs/faq.md +5 -5
- package/docs/manual.html +41 -59
- package/docs/resource-install.md +5 -3
- package/docs/roadmap.md +2 -2
- package/hooks/commit-msg +4 -33
- package/lib/client.js +332 -226
- package/lib/client.js.map +3 -3
- package/lib/controller.d.ts +19 -3
- package/lib/controller.js +132 -28
- package/lib/controller.js.map +1 -1
- package/lib/dev-task.js +130 -79
- package/lib/dev-task.js.map +1 -1
- package/lib/engine.d.ts +10 -13
- package/lib/engine.js +3 -3
- package/lib/engine.js.map +1 -1
- package/lib/shipped-skills.d.ts +2 -4
- package/lib/shipped-skills.js +2 -4
- package/lib/shipped-skills.js.map +1 -1
- package/lib/skill-audit.d.ts +4 -4
- package/lib/skill-audit.js +19 -14
- package/lib/skill-audit.js.map +1 -1
- package/lib/user-skill-profiles.d.ts +7 -0
- package/lib/user-skill-profiles.js +69 -0
- package/lib/user-skill-profiles.js.map +1 -0
- package/lib/workflows.d.ts +24 -4
- package/lib/workflows.js +57 -41
- package/lib/workflows.js.map +1 -1
- package/package.json +4 -4
- package/preset/agent.cordis.yml +3 -3
- package/preset/persona.md +2 -2
- package/scripts/verify-package.mjs +3 -1
- package/skills/eng-delivery/SKILL.md +17 -17
- package/rules/coding-conventions.md +0 -7
- package/rules/commit-conventions.md +0 -6
- package/rules/security-redlines.md +0 -6
- package/skills/code-commit/SKILL.md +0 -13
- package/skills/code-implement/SKILL.md +0 -24
- package/skills/code-review/SKILL.md +0 -13
- package/skills/code-verify/SKILL.md +0 -20
- package/skills/requirement-analysis/SKILL.md +0 -16
- package/skills/solution-design/SKILL.md +0 -19
package/lib/dev-task.js
CHANGED
|
@@ -18,6 +18,7 @@ import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
|
18
18
|
import { assertAdvance, bindingsForStage, checkFileScope, commitCheckpoint, applyRevision, completionBlockers, formatResourceRef, invalidatedBy, legalTargets, resourceBlockers, newTask, taskIdFromMessage, unmetGuards, validateCommitMessage, validateWorkflow, verificationBlockers, } from "./engine.js";
|
|
19
19
|
import { HIGH_RISK_REQUIRED_CAPABILITIES, flowSatisfies, resolveFlow, } from "./workflows.js";
|
|
20
20
|
import { hashConfig, hashText } from "./snapshot.js";
|
|
21
|
+
import { withUserSkillProfiles } from "./user-skill-profiles.js";
|
|
21
22
|
import { loadedSkills, needsSkillReceipt, obligationStages, skillBlockers } from "./skill-audit.js";
|
|
22
23
|
import { detectRoot, detectType, defaultVerifyCommand, listProjectRules, listProjectSkills, presentGovernanceFiles, } from "./project.js";
|
|
23
24
|
/** Bundled commit-msg hook, copied into `.git/hooks/` by the `install_hook` operation. */
|
|
@@ -184,15 +185,23 @@ async function resolveWorkflow(fs, cwd) {
|
|
|
184
185
|
source: 'invalid',
|
|
185
186
|
};
|
|
186
187
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
188
|
+
let project;
|
|
189
|
+
try {
|
|
190
|
+
project = withUserSkillProfiles({
|
|
191
|
+
flow: parsed.flow,
|
|
192
|
+
...(parsed.stage_bindings !== undefined ? { stage_bindings: parsed.stage_bindings } : {}),
|
|
193
|
+
...(parsed.skill_profiles !== undefined ? { skill_profiles: parsed.skill_profiles } : {}),
|
|
194
|
+
...(parsed.commit !== undefined ? { commit: parsed.commit } : {}),
|
|
195
|
+
...(parsed.artifacts !== undefined ? { artifacts: parsed.artifacts } : {}),
|
|
196
|
+
...(parsed.review_depth !== undefined ? { review_depth: parsed.review_depth } : {}),
|
|
197
|
+
...(parsed.commit_required !== undefined ? { commit_required: parsed.commit_required } : {}),
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
const standard = standardWorkflow();
|
|
202
|
+
return { ...standard, problems: [`用户级技能规则档案无法读取:${error instanceof Error ? error.message : String(error)}`], source: 'invalid' };
|
|
203
|
+
}
|
|
204
|
+
const resolved = resolveFlow(parsed.flow, project);
|
|
196
205
|
if (!resolved.ok) {
|
|
197
206
|
const standard = standardWorkflow();
|
|
198
207
|
return {
|
|
@@ -337,19 +346,6 @@ function readAbsRule(file) {
|
|
|
337
346
|
return undefined;
|
|
338
347
|
}
|
|
339
348
|
}
|
|
340
|
-
/** The bundled rule files whose contents the frozen tasks' bindings reference. */
|
|
341
|
-
const BUILTIN_RULE_FILES = ['commit-conventions', 'coding-conventions', 'security-redlines'];
|
|
342
|
-
/**
|
|
343
|
-
* Fingerprint of the bundled rules this package ships. A task created now
|
|
344
|
-
* freezes this value; when a later package version ships changed rules, the
|
|
345
|
-
* disclosure reports the drift instead of silently applying new content.
|
|
346
|
-
*/
|
|
347
|
-
function builtinRulesFingerprint() {
|
|
348
|
-
const contents = BUILTIN_RULE_FILES
|
|
349
|
-
.map(name => readAbsRule(fileURLToPath(new URL(`../rules/${name}.md`, import.meta.url))) ?? '')
|
|
350
|
-
.join('\n');
|
|
351
|
-
return hashText(contents);
|
|
352
|
-
}
|
|
353
349
|
/** Read one rule body from a specific layer, so a reference decides where to look. */
|
|
354
350
|
async function readRuleAt(source, name, fs, cwd) {
|
|
355
351
|
const safe = sanitize(name);
|
|
@@ -395,17 +391,18 @@ async function readSkillAt(ref, fs, cwd) {
|
|
|
395
391
|
return read(join(BUNDLED_SKILLS_DIR, safe, 'SKILL.md'));
|
|
396
392
|
case 'project':
|
|
397
393
|
return await readText(fs, '.dsh/skills/' + safe + '/SKILL.md', cwd);
|
|
394
|
+
case 'codex-project':
|
|
395
|
+
return await readText(fs, '.agents/skills/' + safe + '/SKILL.md', cwd);
|
|
398
396
|
case 'user':
|
|
399
397
|
return read(join(dshHome(), 'skills', safe, 'SKILL.md'));
|
|
400
398
|
}
|
|
401
399
|
}
|
|
402
400
|
/**
|
|
403
|
-
*
|
|
401
|
+
* Capture every skill and rule body the config resolves to at task creation.
|
|
404
402
|
*
|
|
405
|
-
*
|
|
406
|
-
*
|
|
407
|
-
*
|
|
408
|
-
* running task, and identical bodies are stored once per hash.
|
|
403
|
+
* The copy is an audit baseline, not the runtime authority: task interactions
|
|
404
|
+
* always read the latest body at the same source-qualified reference. Capturing
|
|
405
|
+
* hashes makes changes visible and rejects an unreadable binding at creation.
|
|
409
406
|
* @param config - the frozen workflow config.
|
|
410
407
|
* @param fs - sandboxed filesystem for project-level lookups.
|
|
411
408
|
* @returns the frozen resources plus the refs that could not be read.
|
|
@@ -446,8 +443,8 @@ async function freezeResources(config, fs, cwd) {
|
|
|
446
443
|
* Resolve a bare legacy rule name to the layer that precedence selects.
|
|
447
444
|
*
|
|
448
445
|
* Precedence, unchanged from before: bundled wins over project, which wins over
|
|
449
|
-
* user. A
|
|
450
|
-
*
|
|
446
|
+
* user. A source-qualified reference identifies the intended body even when
|
|
447
|
+
* two resource layers use the same name.
|
|
451
448
|
* @param name - the bare rule name from a legacy config.
|
|
452
449
|
* @param fs - sandboxed filesystem for the project-level lookup.
|
|
453
450
|
* @returns the layer that holds this name, or undefined when none does.
|
|
@@ -480,10 +477,8 @@ async function resolveLegacyRuleSource(name, fs, cwd, frozen) {
|
|
|
480
477
|
async function resolveRules(refs, fs, cwd, frozen) {
|
|
481
478
|
const resolved = [];
|
|
482
479
|
const missing = [];
|
|
483
|
-
//
|
|
484
|
-
//
|
|
485
|
-
// so editing a rule silently changed what a running task was told to follow —
|
|
486
|
-
// the exact instability the snapshot exists to prevent.
|
|
480
|
+
// Creation-time copies are an audit baseline. The source-qualified reference
|
|
481
|
+
// remains stable, while its body is read afresh on each interaction.
|
|
487
482
|
const frozenByRef = new Map();
|
|
488
483
|
// Pre-0.26 snapshots have no kind. If such a snapshot reused a skill's ref
|
|
489
484
|
// for a rule, its rule was never frozen; fail closed instead of reading the
|
|
@@ -499,25 +494,15 @@ async function resolveRules(refs, fs, cwd, frozen) {
|
|
|
499
494
|
for (const ref of refs) {
|
|
500
495
|
const key = formatResourceRef(ref);
|
|
501
496
|
const snapshot = frozenByRef.get(key);
|
|
502
|
-
if (snapshot !== undefined) {
|
|
503
|
-
resolved.push({ name: ref.name, content: snapshot.content, source: ref.source });
|
|
504
|
-
// Report when the source no longer matches what was frozen, so drift is
|
|
505
|
-
// visible instead of looking like the frozen text was simply current.
|
|
506
|
-
const live = await readRuleAt(ref.source, ref.name, fs, cwd);
|
|
507
|
-
if (live === undefined)
|
|
508
|
-
stale.push(key + ' (source deleted)');
|
|
509
|
-
else if (hashText(live) !== snapshot.hash)
|
|
510
|
-
stale.push(key + ' (source edited)');
|
|
511
|
-
continue;
|
|
512
|
-
}
|
|
513
|
-
// No snapshot (a pre-freezing task) or this ref was not frozen: read live, and
|
|
514
|
-
// report a rule that resolves nowhere rather than dropping it, because omitting
|
|
515
|
-
// it made a deleted file indistinguishable from an unbound one.
|
|
516
497
|
const content = await readRuleAt(ref.source, ref.name, fs, cwd);
|
|
517
498
|
if (content === undefined) {
|
|
518
499
|
missing.push(key);
|
|
500
|
+
if (snapshot !== undefined)
|
|
501
|
+
stale.push(key + ' (source deleted)');
|
|
519
502
|
continue;
|
|
520
503
|
}
|
|
504
|
+
if (snapshot !== undefined && hashText(content) !== snapshot.hash)
|
|
505
|
+
stale.push(key + ' (source edited)');
|
|
521
506
|
resolved.push({ name: ref.name, content, source: ref.source });
|
|
522
507
|
}
|
|
523
508
|
return { resolved, missing, stale };
|
|
@@ -573,20 +558,45 @@ async function rulesForBinding(binding, fs, cwd, frozen) {
|
|
|
573
558
|
stale: [...stale, ...legacyResult.stale],
|
|
574
559
|
};
|
|
575
560
|
}
|
|
576
|
-
/** Return
|
|
577
|
-
async function skillBodiesForBinding(binding, fs, cwd
|
|
561
|
+
/** Return current skill instructions at each stable source-qualified reference. */
|
|
562
|
+
async function skillBodiesForBinding(binding, fs, cwd) {
|
|
578
563
|
const result = [];
|
|
579
564
|
for (const entry of binding?.skills ?? []) {
|
|
580
|
-
const
|
|
581
|
-
const archived = frozen?.find(resource => (resource.kind === 'skill'
|
|
582
|
-
|| (resource.kind === undefined && resource.content.startsWith('---')))
|
|
583
|
-
&& formatResourceRef(resource.ref) === key);
|
|
584
|
-
const content = archived?.content ?? await readSkillAt(entry.skill, fs, cwd);
|
|
565
|
+
const content = await readSkillAt(entry.skill, fs, cwd);
|
|
585
566
|
if (content !== undefined)
|
|
586
567
|
result.push({ name: entry.skill.name, source: entry.skill.source, content });
|
|
587
568
|
}
|
|
588
569
|
return result;
|
|
589
570
|
}
|
|
571
|
+
/** Missing live skill files cannot be satisfied by the creation-time archive. */
|
|
572
|
+
async function missingSkillRefs(binding, fs, cwd) {
|
|
573
|
+
const missing = [];
|
|
574
|
+
for (const entry of binding?.skills ?? []) {
|
|
575
|
+
if ((await readSkillAt(entry.skill, fs, cwd)) === undefined)
|
|
576
|
+
missing.push(formatResourceRef(entry.skill));
|
|
577
|
+
}
|
|
578
|
+
return missing;
|
|
579
|
+
}
|
|
580
|
+
function skillResourceBlockers(missing, stage) {
|
|
581
|
+
return missing.map(ref => `${stage}: bound skill ${ref} resolves nowhere; restore the file or remove the binding for a new task`);
|
|
582
|
+
}
|
|
583
|
+
async function changedSkillRefs(binding, fs, cwd, baseline) {
|
|
584
|
+
const changed = [];
|
|
585
|
+
for (const entry of binding?.skills ?? []) {
|
|
586
|
+
const key = formatResourceRef(entry.skill);
|
|
587
|
+
const original = baseline?.find(resource => (resource.kind === 'skill'
|
|
588
|
+
|| (resource.kind === undefined && resource.content.startsWith('---')))
|
|
589
|
+
&& formatResourceRef(resource.ref) === key);
|
|
590
|
+
if (original === undefined)
|
|
591
|
+
continue;
|
|
592
|
+
const live = await readSkillAt(entry.skill, fs, cwd);
|
|
593
|
+
if (live === undefined)
|
|
594
|
+
changed.push(key + ' (source deleted)');
|
|
595
|
+
else if (hashText(live) !== original.hash)
|
|
596
|
+
changed.push(key + ' (source edited)');
|
|
597
|
+
}
|
|
598
|
+
return changed;
|
|
599
|
+
}
|
|
590
600
|
/**
|
|
591
601
|
* Render a stage's progressive-disclosure payload: skill names to load via the
|
|
592
602
|
* `skill` tool, plus the resolved rule bodies to follow. Empty text means the
|
|
@@ -603,11 +613,11 @@ async function renderBindings(stage, workflow, fs, cwd, frozen) {
|
|
|
603
613
|
return '';
|
|
604
614
|
const { resolved: rules, missing, legacy } = await rulesForBinding(binding, fs, cwd, frozen);
|
|
605
615
|
const skillPart = skills.length > 0
|
|
606
|
-
? `skills to load: ${skills.map(skill => formatResourceRef(skill.skill)).join(', ')} (use the skill tool by name)`
|
|
616
|
+
? `skills to load: ${skills.map(skill => formatResourceRef(skill.skill)).join(', ')} (use the skill tool by name; for codex-project skills use dev_task operation=load_skill with the qualified skill_name)`
|
|
607
617
|
: 'skills to load: none';
|
|
608
|
-
const
|
|
609
|
-
const skillInstructions =
|
|
610
|
-
? `skill instructions for this task (
|
|
618
|
+
const currentSkills = await skillBodiesForBinding(binding, fs, cwd);
|
|
619
|
+
const skillInstructions = currentSkills.length > 0
|
|
620
|
+
? `current skill instructions for this task (read at this interaction):\n${currentSkills.map(skill => `### ${skill.source}:${skill.name}\n${skill.content}`).join('\n\n')}`
|
|
611
621
|
: '';
|
|
612
622
|
const rulePart = rules.length > 0
|
|
613
623
|
? `rules in force at this stage (each belongs to the skill shown by the binding):\n${rules.map(r => `### ${r.source}:${r.name}\n${r.content}`).join('\n\n')}`
|
|
@@ -617,6 +627,9 @@ async function renderBindings(stage, workflow, fs, cwd, frozen) {
|
|
|
617
627
|
const missingPart = missing.length > 0
|
|
618
628
|
? `rules in force at this stage but NOT FOUND in their declared layer: ${missing.join(', ')}`
|
|
619
629
|
: '';
|
|
630
|
+
const missingSkillPart = skills.length > currentSkills.length
|
|
631
|
+
? `bound skills NOT FOUND in their declared layer: ${(await missingSkillRefs(binding, fs, cwd)).join(', ')}`
|
|
632
|
+
: '';
|
|
620
633
|
// Legacy stage-level rules are disclosed (so nothing is lost) but named as
|
|
621
634
|
// unassigned, because a stage-level list never recorded which skill they were
|
|
622
635
|
// meant for and guessing would invent an answer the config did not contain.
|
|
@@ -630,12 +643,12 @@ async function renderBindings(stage, workflow, fs, cwd, frozen) {
|
|
|
630
643
|
// run something irrelevant.
|
|
631
644
|
const additional = skills
|
|
632
645
|
.filter(entry => entry.evidence === undefined || entry.evidence === 'command')
|
|
633
|
-
.map(entry => entry.skill.name)
|
|
646
|
+
.map(entry => entry.skill.source === 'codex-project' ? formatResourceRef(entry.skill) : entry.skill.name)
|
|
634
647
|
.filter(name => needsSkillReceipt(name));
|
|
635
648
|
const receiptPart = additional.length
|
|
636
649
|
? `additional skills requiring skill_result command receipts: ${additional.join(', ')}`
|
|
637
650
|
: 'no command skill_result receipt required for this stage; other evidence kinds follow their configured stage operations';
|
|
638
|
-
return [skillPart, skillInstructions, receiptPart, rulePart, missingPart, legacyPart].filter(Boolean).join('\n');
|
|
651
|
+
return [skillPart, skillInstructions, receiptPart, rulePart, missingPart, missingSkillPart, legacyPart].filter(Boolean).join('\n');
|
|
639
652
|
}
|
|
640
653
|
/** Normalize the tool's `items` argument into complete TaskItem records. */
|
|
641
654
|
function normalizeItems(items, previous = []) {
|
|
@@ -759,7 +772,7 @@ export async function approveAdvance(ctx, state, target, workflow, confirmations
|
|
|
759
772
|
state.solution_confirmed = true;
|
|
760
773
|
return assertAdvance(state, target, workflow);
|
|
761
774
|
}
|
|
762
|
-
const OPERATIONS = ['status', 'create', 'record', 'items', 'scope', 'dispatch', 'review_item', 'advance', 'verify', 'review', 'commit', 'complete', 'revise', 'config', 'install_hook', 'verify_hook', 'init', 'set_risk', 'skill_result'];
|
|
775
|
+
const OPERATIONS = ['status', 'create', 'load_skill', 'record', 'items', 'scope', 'dispatch', 'review_item', 'advance', 'verify', 'review', 'commit', 'complete', 'revise', 'config', 'install_hook', 'verify_hook', 'init', 'set_risk', 'skill_result'];
|
|
763
776
|
const TOOL_DESCRIPTION = 'Own the engineering delivery workflow as hard state. Read or create the task record, record a ' +
|
|
764
777
|
'stage artifact, record each implementation item\'s subagent dispatch and two-stage (spec + quality) ' +
|
|
765
778
|
'review audit (the flow decides how many verdicts per item), advance one stage (rejected unless every configured guard already holds, including ' +
|
|
@@ -784,7 +797,7 @@ export function registerDevTask(ctx) {
|
|
|
784
797
|
parameters: {
|
|
785
798
|
operation: { type: 'string', enum: [...OPERATIONS], required: true, description: 'Which task-record action to perform.' },
|
|
786
799
|
task_id: { type: 'string', description: 'Task id. Omit on status to discover tasks in the current workspace; optionally filter by branch.' },
|
|
787
|
-
skill_name: { type: 'string', description: 'Bound skill to record
|
|
800
|
+
skill_name: { type: 'string', description: 'Bound skill to load or record. For a codex-project skill, pass codex-project:name to load_skill and skill_result. Command evidence needs a real validation command; manual evidence needs human approval.' },
|
|
788
801
|
branch: { type: 'string', description: 'Current git branch (recorded on create).' },
|
|
789
802
|
title: { type: 'string', description: 'Task title (create).' },
|
|
790
803
|
work_size: { type: 'string', enum: ['tiny', 'standard', 'complex'], description: 'Workload tier (create).' },
|
|
@@ -1000,7 +1013,6 @@ export function registerDevTask(ctx) {
|
|
|
1000
1013
|
state.items = normalizeItems(a.items);
|
|
1001
1014
|
state.execution_version = 1;
|
|
1002
1015
|
state.files = a.files ?? [];
|
|
1003
|
-
state.bindings_fingerprint = builtinRulesFingerprint();
|
|
1004
1016
|
assertInsideRoot(root, cwd ?? '', taskPath(state.id));
|
|
1005
1017
|
await writeTask(fs, state, cwd, await resolveWriteMode(ctx, a, exec));
|
|
1006
1018
|
return `created ${state.id} at stage ${state.stage}; legal next: ${legalTargets(state.stage, flow.config).join(', ') || 'none'}\n${await renderBindings(state.stage, flow.config, fs, cwd, flow.resources)}\nBefore advance, load all bound skills. Record each skill's declared evidence. Terminal bindings must finish before entering the terminal stage.`;
|
|
@@ -1015,9 +1027,28 @@ export function registerDevTask(ctx) {
|
|
|
1015
1027
|
if (state.completed !== undefined && a.operation !== 'status' && a.operation !== 'revise') {
|
|
1016
1028
|
throw new Error('task is completed; use revise to reopen it before changing task state');
|
|
1017
1029
|
}
|
|
1030
|
+
if (a.operation === 'load_skill') {
|
|
1031
|
+
const stage = a.target_stage ?? state.stage;
|
|
1032
|
+
if (!obligationStages(state, workflow).includes(stage))
|
|
1033
|
+
throw new Error(`stage "${stage}" is not currently actionable`);
|
|
1034
|
+
const entry = (workflow.stage_bindings?.[stage]?.skills ?? [])
|
|
1035
|
+
.find(binding => formatResourceRef(binding.skill) === a.skill_name);
|
|
1036
|
+
if (!entry || entry.skill.source !== 'codex-project')
|
|
1037
|
+
throw new Error('load_skill requires a bound codex-project:name at the current or upcoming terminal stage');
|
|
1038
|
+
const content = await readSkillAt(entry.skill, fs, cwd);
|
|
1039
|
+
if (content === undefined)
|
|
1040
|
+
throw new Error(`bound skill ${a.skill_name} cannot be read`);
|
|
1041
|
+
const { resolved, missing } = await resolveRules(entry.rules, fs, cwd, state.flow?.resources);
|
|
1042
|
+
if (missing.length)
|
|
1043
|
+
throw new Error(`bound rule for ${a.skill_name} cannot be read: ${missing.join(', ')}`);
|
|
1044
|
+
return [`skill: ${a.skill_name}`, `current skill instructions:\n${content}`,
|
|
1045
|
+
resolved.length ? `rules attached to this skill:\n${resolved.map(rule => `### ${rule.source}:${rule.name}\n${rule.content}`).join('\n\n')}` : 'rules attached to this skill: none'].join('\n\n');
|
|
1046
|
+
}
|
|
1018
1047
|
if (a.operation === 'status') {
|
|
1019
1048
|
const binding = bindingsForStage(state.stage, workflow);
|
|
1020
1049
|
const { resolved: rules, missing: missingRules, legacy: legacyRules, stale: staleRules, } = await rulesForBinding(binding, fs, cwd, state.flow?.resources);
|
|
1050
|
+
const missingSkillFiles = await missingSkillRefs(binding, fs, cwd);
|
|
1051
|
+
const changedSkillFiles = await changedSkillRefs(binding, fs, cwd, state.flow?.resources);
|
|
1021
1052
|
const missingSkills = skillBlockers(state, workflow, session);
|
|
1022
1053
|
const staleEvidence = await evidenceBlockers(fs, state, workflow, cwd);
|
|
1023
1054
|
const checkpoint = commitCheckpoint(state, workflow);
|
|
@@ -1025,9 +1056,12 @@ export function registerDevTask(ctx) {
|
|
|
1025
1056
|
// one says the receipt no longer matches the tree, the other says the flow
|
|
1026
1057
|
// needs a passing verification and does not have one. Callers act on both.
|
|
1027
1058
|
const verification = verificationBlockers(state, workflow);
|
|
1028
|
-
const commitBlockers = [...staleEvidence, ...missingSkills,
|
|
1059
|
+
const commitBlockers = [...staleEvidence, ...missingSkills,
|
|
1060
|
+
...skillResourceBlockers(missingSkillFiles, state.stage), ...verification];
|
|
1029
1061
|
const completionIssues = state.completed === undefined
|
|
1030
|
-
? [...completionBlockers(state, workflow), ...missingSkills,
|
|
1062
|
+
? [...completionBlockers(state, workflow), ...missingSkills,
|
|
1063
|
+
...skillResourceBlockers(missingSkillFiles, state.stage),
|
|
1064
|
+
...resourceBlockers(missingRules, state.stage), ...staleEvidence]
|
|
1031
1065
|
: [];
|
|
1032
1066
|
return JSON.stringify({
|
|
1033
1067
|
id: state.id,
|
|
@@ -1043,12 +1077,12 @@ export function registerDevTask(ctx) {
|
|
|
1043
1077
|
items: state.items,
|
|
1044
1078
|
skill_obligations: obligationStages(state, workflow).map(stage => {
|
|
1045
1079
|
const bindings = workflow.stage_bindings?.[stage]?.skills ?? [];
|
|
1046
|
-
const skills = bindings.map(entry => entry.skill.name);
|
|
1080
|
+
const skills = bindings.map(entry => entry.skill.source === 'codex-project' ? formatResourceRef(entry.skill) : entry.skill.name);
|
|
1047
1081
|
// Only bindings whose evidence is a command owe a command receipt, so a
|
|
1048
1082
|
// document-producing skill is not told to run something irrelevant.
|
|
1049
1083
|
const command_receipts_required = bindings
|
|
1050
1084
|
.filter(entry => entry.evidence === undefined || entry.evidence === 'command')
|
|
1051
|
-
.map(entry => entry.skill.name)
|
|
1085
|
+
.map(entry => entry.skill.source === 'codex-project' ? formatResourceRef(entry.skill) : entry.skill.name)
|
|
1052
1086
|
.filter(name => needsSkillReceipt(name));
|
|
1053
1087
|
return { stage, skills, command_receipts_required };
|
|
1054
1088
|
}),
|
|
@@ -1060,12 +1094,15 @@ export function registerDevTask(ctx) {
|
|
|
1060
1094
|
// awaiting an owner. A skill's own list is the complete answer to what it
|
|
1061
1095
|
// runs under, and this mirrors that list for the current stage.
|
|
1062
1096
|
missing_rules: missingRules,
|
|
1097
|
+
missing_skills: missingSkillFiles,
|
|
1098
|
+
changed_source_skills: changedSkillFiles,
|
|
1063
1099
|
rules: rules.map(rule => ({ name: rule.name, source: rule.source })),
|
|
1064
|
-
// Rules
|
|
1065
|
-
//
|
|
1066
|
-
// the drift, so a rule edited or deleted underneath a running task is visible
|
|
1067
|
-
// instead of silently authoritative.
|
|
1100
|
+
// Rules that differ from the creation-time audit copy. Edited bodies
|
|
1101
|
+
// are already in force; deleted sources are reported as missing.
|
|
1068
1102
|
stale_source_rules: staleRules,
|
|
1103
|
+
resource_update_notice: changedSkillFiles.length || staleRules.length
|
|
1104
|
+
? 'Skill/Rule source changed since task creation; the current source body is in force. Recheck prior evidence against the updated instructions.'
|
|
1105
|
+
: undefined,
|
|
1069
1106
|
unassigned_legacy_rules: legacyRules,
|
|
1070
1107
|
skill_results: state.skill_results ?? {},
|
|
1071
1108
|
commits: state.commits,
|
|
@@ -1083,16 +1120,13 @@ export function registerDevTask(ctx) {
|
|
|
1083
1120
|
: checkpoint,
|
|
1084
1121
|
bindings: {
|
|
1085
1122
|
skills: binding?.skills ?? [],
|
|
1086
|
-
skill_contents: await skillBodiesForBinding(binding, fs, cwd
|
|
1123
|
+
skill_contents: await skillBodiesForBinding(binding, fs, cwd),
|
|
1087
1124
|
// The layer is disclosed with each rule, because two same-named rules in
|
|
1088
1125
|
// different layers are distinct resources and the caller needs to know
|
|
1089
1126
|
// which one is actually in force. Reporting only the name made them
|
|
1090
1127
|
// indistinguishable in exactly the case where the distinction matters.
|
|
1091
1128
|
rules: rules.map(r => ({ name: r.name, source: r.source, content: r.content })),
|
|
1092
1129
|
},
|
|
1093
|
-
bindings_drift: state.bindings_fingerprint !== undefined && state.bindings_fingerprint !== builtinRulesFingerprint()
|
|
1094
|
-
? 'bundled rules changed since this task froze — the frozen fingerprint no longer matches the shipped rules'
|
|
1095
|
-
: undefined,
|
|
1096
1130
|
risk_downgrades: state.risk_downgrades ?? [],
|
|
1097
1131
|
}, null, 2);
|
|
1098
1132
|
}
|
|
@@ -1152,10 +1186,13 @@ export function registerDevTask(ctx) {
|
|
|
1152
1186
|
// terminal stage has no way out.
|
|
1153
1187
|
await assertFreshEvidence(fs, state, workflow, cwd);
|
|
1154
1188
|
const missingSkills = skillBlockers(state, workflow, session);
|
|
1155
|
-
const
|
|
1189
|
+
const currentBinding = bindingsForStage(state.stage, workflow);
|
|
1190
|
+
const missingSkillFiles = await missingSkillRefs(currentBinding, fs, cwd);
|
|
1191
|
+
const unresolvedRules = (await rulesForBinding(currentBinding, fs, cwd, state.flow?.resources)).missing;
|
|
1156
1192
|
const blockers = [
|
|
1157
1193
|
...completionBlockers(state, workflow),
|
|
1158
1194
|
...missingSkills,
|
|
1195
|
+
...skillResourceBlockers(missingSkillFiles, state.stage),
|
|
1159
1196
|
...resourceBlockers(unresolvedRules, state.stage),
|
|
1160
1197
|
];
|
|
1161
1198
|
if (blockers.length > 0)
|
|
@@ -1264,6 +1301,10 @@ export function registerDevTask(ctx) {
|
|
|
1264
1301
|
const missingSkills = skillBlockers(state, workflow, session);
|
|
1265
1302
|
if (missingSkills.length)
|
|
1266
1303
|
throw new Error(missingSkills.join('; '));
|
|
1304
|
+
const missingSkillFiles = await missingSkillRefs(bindingsForStage(state.stage, workflow), fs, cwd);
|
|
1305
|
+
const missingSkillIssues = skillResourceBlockers(missingSkillFiles, state.stage);
|
|
1306
|
+
if (missingSkillIssues.length)
|
|
1307
|
+
throw new Error(missingSkillIssues.join('; '));
|
|
1267
1308
|
// A stage whose rules cannot be resolved must not be passable. This was
|
|
1268
1309
|
// reported in status and never enforced, so deleting a bound rule still let the
|
|
1269
1310
|
// task advance — the stage ran without the constraints it was configured with
|
|
@@ -1365,12 +1406,22 @@ export function registerDevTask(ctx) {
|
|
|
1365
1406
|
}
|
|
1366
1407
|
case 'skill_result': {
|
|
1367
1408
|
const stage = a.target_stage ?? state.stage;
|
|
1368
|
-
const entry = (workflow.stage_bindings?.[stage]?.skills ?? []).find(binding => binding.skill.name === a.skill_name);
|
|
1409
|
+
const entry = (workflow.stage_bindings?.[stage]?.skills ?? []).find(binding => (binding.skill.source === 'codex-project' ? formatResourceRef(binding.skill) : binding.skill.name) === a.skill_name);
|
|
1369
1410
|
if (!obligationStages(state, workflow).includes(stage) || !entry)
|
|
1370
1411
|
throw new Error('skill_result requires a skill bound to this stage or its upcoming terminal stage');
|
|
1371
|
-
|
|
1412
|
+
if ((await readSkillAt(entry.skill, fs, cwd)) === undefined) {
|
|
1413
|
+
throw new Error(`bound skill ${formatResourceRef(entry.skill)} resolves nowhere; restore its source before recording a result`);
|
|
1414
|
+
}
|
|
1415
|
+
const unresolvedForSkill = (await resolveRules(entry.rules, fs, cwd, state.flow?.resources)).missing;
|
|
1416
|
+
if (unresolvedForSkill.length > 0) {
|
|
1417
|
+
throw new Error(`bound rule for ${formatResourceRef(entry.skill)} resolves nowhere: ${unresolvedForSkill.join(', ')}`);
|
|
1418
|
+
}
|
|
1419
|
+
const loadKey = entry.skill.source === 'codex-project' ? `${state.id}#${a.skill_name}` : a.skill_name;
|
|
1420
|
+
const loadCall = loadedSkills(session).get(loadKey);
|
|
1372
1421
|
if (!loadCall)
|
|
1373
|
-
throw new Error(
|
|
1422
|
+
throw new Error(entry.skill.source === 'codex-project'
|
|
1423
|
+
? `load skill "${a.skill_name}" with dev_task operation=load_skill before recording execution`
|
|
1424
|
+
: `load skill "${a.skill_name}" successfully before recording execution`);
|
|
1374
1425
|
if (entry.evidence === 'manual') {
|
|
1375
1426
|
if (!a.evidence?.length || a.evidence.some(value => !value.trim()))
|
|
1376
1427
|
throw new Error('manual skill_result requires non-empty evidence');
|