@godv61/dsh-task-engine 0.26.0 → 0.27.0
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/.enforce-test.mjs +8 -12
- package/.evidence-test.mjs +5 -5
- package/.freeze-test.mjs +24 -29
- package/.revision-test.mjs +16 -16
- package/.roundtrip-test.mjs +259 -120
- package/.workflow-test.mjs +113 -94
- package/README.md +9 -9
- package/defaults/eng.json +97 -97
- package/docs/CHANGELOG.md +26 -13
- package/docs/configuration.md +20 -16
- package/docs/development.md +1 -1
- package/docs/faq.md +5 -5
- package/docs/manual.html +33 -33
- package/docs/roadmap.md +2 -2
- package/lib/client.js +303 -139
- package/lib/client.js.map +2 -2
- package/lib/controller.d.ts +15 -0
- package/lib/controller.js +120 -22
- package/lib/controller.js.map +1 -1
- package/lib/dev-task.js +105 -60
- package/lib/dev-task.js.map +1 -1
- package/lib/engine.d.ts +8 -10
- package/lib/engine.js +1 -1
- package/lib/engine.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 +21 -0
- package/lib/workflows.js +55 -0
- package/lib/workflows.js.map +1 -1
- package/package.json +1 -1
- package/preset/agent.cordis.yml +3 -3
- package/preset/persona.md +2 -2
- package/skills/code-implement/SKILL.md +1 -1
- package/skills/eng-delivery/SKILL.md +17 -38
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,12 +346,11 @@ function readAbsRule(file) {
|
|
|
337
346
|
return undefined;
|
|
338
347
|
}
|
|
339
348
|
}
|
|
340
|
-
/** The bundled rule files whose
|
|
349
|
+
/** The bundled rule files whose changes can be reported to existing tasks. */
|
|
341
350
|
const BUILTIN_RULE_FILES = ['commit-conventions', 'coding-conventions', 'security-redlines'];
|
|
342
351
|
/**
|
|
343
|
-
* Fingerprint of the bundled rules this package ships.
|
|
344
|
-
*
|
|
345
|
-
* disclosure reports the drift instead of silently applying new content.
|
|
352
|
+
* Fingerprint of the bundled rules this package ships. Existing tasks record
|
|
353
|
+
* the creation-time value so changes can be reported while live bodies apply.
|
|
346
354
|
*/
|
|
347
355
|
function builtinRulesFingerprint() {
|
|
348
356
|
const contents = BUILTIN_RULE_FILES
|
|
@@ -400,12 +408,11 @@ async function readSkillAt(ref, fs, cwd) {
|
|
|
400
408
|
}
|
|
401
409
|
}
|
|
402
410
|
/**
|
|
403
|
-
*
|
|
411
|
+
* Capture every skill and rule body the config resolves to at task creation.
|
|
404
412
|
*
|
|
405
|
-
*
|
|
406
|
-
*
|
|
407
|
-
*
|
|
408
|
-
* running task, and identical bodies are stored once per hash.
|
|
413
|
+
* The copy is an audit baseline, not the runtime authority: task interactions
|
|
414
|
+
* always read the latest body at the same source-qualified reference. Capturing
|
|
415
|
+
* hashes makes changes visible and rejects an unreadable binding at creation.
|
|
409
416
|
* @param config - the frozen workflow config.
|
|
410
417
|
* @param fs - sandboxed filesystem for project-level lookups.
|
|
411
418
|
* @returns the frozen resources plus the refs that could not be read.
|
|
@@ -480,10 +487,8 @@ async function resolveLegacyRuleSource(name, fs, cwd, frozen) {
|
|
|
480
487
|
async function resolveRules(refs, fs, cwd, frozen) {
|
|
481
488
|
const resolved = [];
|
|
482
489
|
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.
|
|
490
|
+
// Creation-time copies are an audit baseline. The source-qualified reference
|
|
491
|
+
// remains stable, while its body is read afresh on each interaction.
|
|
487
492
|
const frozenByRef = new Map();
|
|
488
493
|
// Pre-0.26 snapshots have no kind. If such a snapshot reused a skill's ref
|
|
489
494
|
// for a rule, its rule was never frozen; fail closed instead of reading the
|
|
@@ -499,25 +504,15 @@ async function resolveRules(refs, fs, cwd, frozen) {
|
|
|
499
504
|
for (const ref of refs) {
|
|
500
505
|
const key = formatResourceRef(ref);
|
|
501
506
|
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
507
|
const content = await readRuleAt(ref.source, ref.name, fs, cwd);
|
|
517
508
|
if (content === undefined) {
|
|
518
509
|
missing.push(key);
|
|
510
|
+
if (snapshot !== undefined)
|
|
511
|
+
stale.push(key + ' (source deleted)');
|
|
519
512
|
continue;
|
|
520
513
|
}
|
|
514
|
+
if (snapshot !== undefined && hashText(content) !== snapshot.hash)
|
|
515
|
+
stale.push(key + ' (source edited)');
|
|
521
516
|
resolved.push({ name: ref.name, content, source: ref.source });
|
|
522
517
|
}
|
|
523
518
|
return { resolved, missing, stale };
|
|
@@ -573,20 +568,45 @@ async function rulesForBinding(binding, fs, cwd, frozen) {
|
|
|
573
568
|
stale: [...stale, ...legacyResult.stale],
|
|
574
569
|
};
|
|
575
570
|
}
|
|
576
|
-
/** Return
|
|
577
|
-
async function skillBodiesForBinding(binding, fs, cwd
|
|
571
|
+
/** Return current skill instructions at each stable source-qualified reference. */
|
|
572
|
+
async function skillBodiesForBinding(binding, fs, cwd) {
|
|
578
573
|
const result = [];
|
|
579
574
|
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);
|
|
575
|
+
const content = await readSkillAt(entry.skill, fs, cwd);
|
|
585
576
|
if (content !== undefined)
|
|
586
577
|
result.push({ name: entry.skill.name, source: entry.skill.source, content });
|
|
587
578
|
}
|
|
588
579
|
return result;
|
|
589
580
|
}
|
|
581
|
+
/** Missing live skill files cannot be satisfied by the creation-time archive. */
|
|
582
|
+
async function missingSkillRefs(binding, fs, cwd) {
|
|
583
|
+
const missing = [];
|
|
584
|
+
for (const entry of binding?.skills ?? []) {
|
|
585
|
+
if ((await readSkillAt(entry.skill, fs, cwd)) === undefined)
|
|
586
|
+
missing.push(formatResourceRef(entry.skill));
|
|
587
|
+
}
|
|
588
|
+
return missing;
|
|
589
|
+
}
|
|
590
|
+
function skillResourceBlockers(missing, stage) {
|
|
591
|
+
return missing.map(ref => `${stage}: bound skill ${ref} resolves nowhere; restore the file or remove the binding for a new task`);
|
|
592
|
+
}
|
|
593
|
+
async function changedSkillRefs(binding, fs, cwd, baseline) {
|
|
594
|
+
const changed = [];
|
|
595
|
+
for (const entry of binding?.skills ?? []) {
|
|
596
|
+
const key = formatResourceRef(entry.skill);
|
|
597
|
+
const original = baseline?.find(resource => (resource.kind === 'skill'
|
|
598
|
+
|| (resource.kind === undefined && resource.content.startsWith('---')))
|
|
599
|
+
&& formatResourceRef(resource.ref) === key);
|
|
600
|
+
if (original === undefined)
|
|
601
|
+
continue;
|
|
602
|
+
const live = await readSkillAt(entry.skill, fs, cwd);
|
|
603
|
+
if (live === undefined)
|
|
604
|
+
changed.push(key + ' (source deleted)');
|
|
605
|
+
else if (hashText(live) !== original.hash)
|
|
606
|
+
changed.push(key + ' (source edited)');
|
|
607
|
+
}
|
|
608
|
+
return changed;
|
|
609
|
+
}
|
|
590
610
|
/**
|
|
591
611
|
* Render a stage's progressive-disclosure payload: skill names to load via the
|
|
592
612
|
* `skill` tool, plus the resolved rule bodies to follow. Empty text means the
|
|
@@ -605,9 +625,9 @@ async function renderBindings(stage, workflow, fs, cwd, frozen) {
|
|
|
605
625
|
const skillPart = skills.length > 0
|
|
606
626
|
? `skills to load: ${skills.map(skill => formatResourceRef(skill.skill)).join(', ')} (use the skill tool by name)`
|
|
607
627
|
: 'skills to load: none';
|
|
608
|
-
const
|
|
609
|
-
const skillInstructions =
|
|
610
|
-
? `skill instructions for this task (
|
|
628
|
+
const currentSkills = await skillBodiesForBinding(binding, fs, cwd);
|
|
629
|
+
const skillInstructions = currentSkills.length > 0
|
|
630
|
+
? `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
631
|
: '';
|
|
612
632
|
const rulePart = rules.length > 0
|
|
613
633
|
? `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,12 +637,15 @@ async function renderBindings(stage, workflow, fs, cwd, frozen) {
|
|
|
617
637
|
const missingPart = missing.length > 0
|
|
618
638
|
? `rules in force at this stage but NOT FOUND in their declared layer: ${missing.join(', ')}`
|
|
619
639
|
: '';
|
|
640
|
+
const missingSkillPart = skills.length > currentSkills.length
|
|
641
|
+
? `bound skills NOT FOUND in their declared layer: ${(await missingSkillRefs(binding, fs, cwd)).join(', ')}`
|
|
642
|
+
: '';
|
|
620
643
|
// Legacy stage-level rules are disclosed (so nothing is lost) but named as
|
|
621
644
|
// unassigned, because a stage-level list never recorded which skill they were
|
|
622
645
|
// meant for and guessing would invent an answer the config did not contain.
|
|
623
646
|
const legacyPart = legacy.length > 0
|
|
624
647
|
? `UNASSIGNED legacy stage rules: ${legacy.join(', ')} — these belong to the stage, not to any skill. ` +
|
|
625
|
-
'Assign each to the skill that should carry it (or copy the skill and give each copy its own rules) and record it in
|
|
648
|
+
'Assign each to the skill that should carry it (or copy the skill and give each copy its own rules) and record it in skill_profiles.'
|
|
626
649
|
: '';
|
|
627
650
|
// The disclosure names the skills that owe a command receipt, honouring each
|
|
628
651
|
// binding's declared evidence kind — a document-producing skill owes an
|
|
@@ -634,8 +657,8 @@ async function renderBindings(stage, workflow, fs, cwd, frozen) {
|
|
|
634
657
|
.filter(name => needsSkillReceipt(name));
|
|
635
658
|
const receiptPart = additional.length
|
|
636
659
|
? `additional skills requiring skill_result command receipts: ${additional.join(', ')}`
|
|
637
|
-
: 'skill_result
|
|
638
|
-
return [skillPart, skillInstructions, receiptPart, rulePart, missingPart, legacyPart].filter(Boolean).join('\n');
|
|
660
|
+
: 'no command skill_result receipt required for this stage; other evidence kinds follow their configured stage operations';
|
|
661
|
+
return [skillPart, skillInstructions, receiptPart, rulePart, missingPart, missingSkillPart, legacyPart].filter(Boolean).join('\n');
|
|
639
662
|
}
|
|
640
663
|
/** Normalize the tool's `items` argument into complete TaskItem records. */
|
|
641
664
|
function normalizeItems(items, previous = []) {
|
|
@@ -784,7 +807,7 @@ export function registerDevTask(ctx) {
|
|
|
784
807
|
parameters: {
|
|
785
808
|
operation: { type: 'string', enum: [...OPERATIONS], required: true, description: 'Which task-record action to perform.' },
|
|
786
809
|
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 after executing it (skill_result): requires prior successful skill load
|
|
810
|
+
skill_name: { type: 'string', description: 'Bound skill to record after executing it (skill_result): requires prior successful skill load. Command evidence needs a real validation command; manual evidence needs human approval. target_stage may name the upcoming terminal stage.' },
|
|
788
811
|
branch: { type: 'string', description: 'Current git branch (recorded on create).' },
|
|
789
812
|
title: { type: 'string', description: 'Task title (create).' },
|
|
790
813
|
work_size: { type: 'string', enum: ['tiny', 'standard', 'complex'], description: 'Workload tier (create).' },
|
|
@@ -808,7 +831,7 @@ export function registerDevTask(ctx) {
|
|
|
808
831
|
quality_outcome: { type: 'string', enum: ['pass', 'fail'], description: 'Code-quality verdict (review_item).' },
|
|
809
832
|
notes: { type: 'array', items: { type: 'string' }, description: 'Findings or defects (review_item).' },
|
|
810
833
|
target_stage: { type: 'string', description: 'Stage to advance to (advance), a bound stage for skill_result, or the stage to return to (revise). For revise it must be the stage the changed decision belongs to. Current/upcoming terminal skill binding stage for skill_result.' },
|
|
811
|
-
command: { type: 'string', description: 'Real acceptance command for verify
|
|
834
|
+
command: { type: 'string', description: 'Real acceptance command for verify or a command-evidence skill_result. New task verification requires a command receipt; other skills follow their configured evidence kind. Propagate failures when composing shell commands.' },
|
|
812
835
|
passed: { type: 'boolean', description: 'Legacy tasks only: verification claim when no command can be resolved. New tasks require real command receipts.' },
|
|
813
836
|
evidence: { type: 'array', items: { type: 'string' }, description: 'Supplementary verification evidence (verify).' },
|
|
814
837
|
outcome: { type: 'string', enum: ['pass', 'blocked'], description: 'Review outcome (review).' },
|
|
@@ -1018,6 +1041,8 @@ export function registerDevTask(ctx) {
|
|
|
1018
1041
|
if (a.operation === 'status') {
|
|
1019
1042
|
const binding = bindingsForStage(state.stage, workflow);
|
|
1020
1043
|
const { resolved: rules, missing: missingRules, legacy: legacyRules, stale: staleRules, } = await rulesForBinding(binding, fs, cwd, state.flow?.resources);
|
|
1044
|
+
const missingSkillFiles = await missingSkillRefs(binding, fs, cwd);
|
|
1045
|
+
const changedSkillFiles = await changedSkillRefs(binding, fs, cwd, state.flow?.resources);
|
|
1021
1046
|
const missingSkills = skillBlockers(state, workflow, session);
|
|
1022
1047
|
const staleEvidence = await evidenceBlockers(fs, state, workflow, cwd);
|
|
1023
1048
|
const checkpoint = commitCheckpoint(state, workflow);
|
|
@@ -1025,9 +1050,12 @@ export function registerDevTask(ctx) {
|
|
|
1025
1050
|
// one says the receipt no longer matches the tree, the other says the flow
|
|
1026
1051
|
// needs a passing verification and does not have one. Callers act on both.
|
|
1027
1052
|
const verification = verificationBlockers(state, workflow);
|
|
1028
|
-
const commitBlockers = [...staleEvidence, ...missingSkills,
|
|
1053
|
+
const commitBlockers = [...staleEvidence, ...missingSkills,
|
|
1054
|
+
...skillResourceBlockers(missingSkillFiles, state.stage), ...verification];
|
|
1029
1055
|
const completionIssues = state.completed === undefined
|
|
1030
|
-
? [...completionBlockers(state, workflow), ...missingSkills,
|
|
1056
|
+
? [...completionBlockers(state, workflow), ...missingSkills,
|
|
1057
|
+
...skillResourceBlockers(missingSkillFiles, state.stage),
|
|
1058
|
+
...resourceBlockers(missingRules, state.stage), ...staleEvidence]
|
|
1031
1059
|
: [];
|
|
1032
1060
|
return JSON.stringify({
|
|
1033
1061
|
id: state.id,
|
|
@@ -1060,12 +1088,15 @@ export function registerDevTask(ctx) {
|
|
|
1060
1088
|
// awaiting an owner. A skill's own list is the complete answer to what it
|
|
1061
1089
|
// runs under, and this mirrors that list for the current stage.
|
|
1062
1090
|
missing_rules: missingRules,
|
|
1091
|
+
missing_skills: missingSkillFiles,
|
|
1092
|
+
changed_source_skills: changedSkillFiles,
|
|
1063
1093
|
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.
|
|
1094
|
+
// Rules that differ from the creation-time audit copy. Edited bodies
|
|
1095
|
+
// are already in force; deleted sources are reported as missing.
|
|
1068
1096
|
stale_source_rules: staleRules,
|
|
1097
|
+
resource_update_notice: changedSkillFiles.length || staleRules.length
|
|
1098
|
+
? 'Skill/Rule source changed since task creation; the current source body is in force. Recheck prior evidence against the updated instructions.'
|
|
1099
|
+
: undefined,
|
|
1069
1100
|
unassigned_legacy_rules: legacyRules,
|
|
1070
1101
|
skill_results: state.skill_results ?? {},
|
|
1071
1102
|
commits: state.commits,
|
|
@@ -1083,7 +1114,7 @@ export function registerDevTask(ctx) {
|
|
|
1083
1114
|
: checkpoint,
|
|
1084
1115
|
bindings: {
|
|
1085
1116
|
skills: binding?.skills ?? [],
|
|
1086
|
-
skill_contents: await skillBodiesForBinding(binding, fs, cwd
|
|
1117
|
+
skill_contents: await skillBodiesForBinding(binding, fs, cwd),
|
|
1087
1118
|
// The layer is disclosed with each rule, because two same-named rules in
|
|
1088
1119
|
// different layers are distinct resources and the caller needs to know
|
|
1089
1120
|
// which one is actually in force. Reporting only the name made them
|
|
@@ -1091,7 +1122,7 @@ export function registerDevTask(ctx) {
|
|
|
1091
1122
|
rules: rules.map(r => ({ name: r.name, source: r.source, content: r.content })),
|
|
1092
1123
|
},
|
|
1093
1124
|
bindings_drift: state.bindings_fingerprint !== undefined && state.bindings_fingerprint !== builtinRulesFingerprint()
|
|
1094
|
-
? 'bundled rules changed since
|
|
1125
|
+
? 'bundled rules changed since task creation — current bundled bodies are now in force'
|
|
1095
1126
|
: undefined,
|
|
1096
1127
|
risk_downgrades: state.risk_downgrades ?? [],
|
|
1097
1128
|
}, null, 2);
|
|
@@ -1152,10 +1183,13 @@ export function registerDevTask(ctx) {
|
|
|
1152
1183
|
// terminal stage has no way out.
|
|
1153
1184
|
await assertFreshEvidence(fs, state, workflow, cwd);
|
|
1154
1185
|
const missingSkills = skillBlockers(state, workflow, session);
|
|
1155
|
-
const
|
|
1186
|
+
const currentBinding = bindingsForStage(state.stage, workflow);
|
|
1187
|
+
const missingSkillFiles = await missingSkillRefs(currentBinding, fs, cwd);
|
|
1188
|
+
const unresolvedRules = (await rulesForBinding(currentBinding, fs, cwd, state.flow?.resources)).missing;
|
|
1156
1189
|
const blockers = [
|
|
1157
1190
|
...completionBlockers(state, workflow),
|
|
1158
1191
|
...missingSkills,
|
|
1192
|
+
...skillResourceBlockers(missingSkillFiles, state.stage),
|
|
1159
1193
|
...resourceBlockers(unresolvedRules, state.stage),
|
|
1160
1194
|
];
|
|
1161
1195
|
if (blockers.length > 0)
|
|
@@ -1264,6 +1298,10 @@ export function registerDevTask(ctx) {
|
|
|
1264
1298
|
const missingSkills = skillBlockers(state, workflow, session);
|
|
1265
1299
|
if (missingSkills.length)
|
|
1266
1300
|
throw new Error(missingSkills.join('; '));
|
|
1301
|
+
const missingSkillFiles = await missingSkillRefs(bindingsForStage(state.stage, workflow), fs, cwd);
|
|
1302
|
+
const missingSkillIssues = skillResourceBlockers(missingSkillFiles, state.stage);
|
|
1303
|
+
if (missingSkillIssues.length)
|
|
1304
|
+
throw new Error(missingSkillIssues.join('; '));
|
|
1267
1305
|
// A stage whose rules cannot be resolved must not be passable. This was
|
|
1268
1306
|
// reported in status and never enforced, so deleting a bound rule still let the
|
|
1269
1307
|
// task advance — the stage ran without the constraints it was configured with
|
|
@@ -1368,6 +1406,13 @@ export function registerDevTask(ctx) {
|
|
|
1368
1406
|
const entry = (workflow.stage_bindings?.[stage]?.skills ?? []).find(binding => binding.skill.name === a.skill_name);
|
|
1369
1407
|
if (!obligationStages(state, workflow).includes(stage) || !entry)
|
|
1370
1408
|
throw new Error('skill_result requires a skill bound to this stage or its upcoming terminal stage');
|
|
1409
|
+
if ((await readSkillAt(entry.skill, fs, cwd)) === undefined) {
|
|
1410
|
+
throw new Error(`bound skill ${formatResourceRef(entry.skill)} resolves nowhere; restore its source before recording a result`);
|
|
1411
|
+
}
|
|
1412
|
+
const unresolvedForSkill = (await resolveRules(entry.rules, fs, cwd, state.flow?.resources)).missing;
|
|
1413
|
+
if (unresolvedForSkill.length > 0) {
|
|
1414
|
+
throw new Error(`bound rule for ${formatResourceRef(entry.skill)} resolves nowhere: ${unresolvedForSkill.join(', ')}`);
|
|
1415
|
+
}
|
|
1371
1416
|
const loadCall = loadedSkills(session).get(a.skill_name);
|
|
1372
1417
|
if (!loadCall)
|
|
1373
1418
|
throw new Error(`load skill "${a.skill_name}" successfully before recording execution`);
|