@godv61/dsh-task-engine 0.28.0 → 0.29.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/.adaptive-test.mjs +189 -0
- package/.hook-test.mjs +19 -2
- package/.workflow-test.mjs +6 -4
- package/README.md +22 -16
- package/docs/CHANGELOG.md +9 -1
- package/docs/README.md +6 -5
- package/docs/adaptive-workflows.md +72 -0
- package/docs/configuration.md +4 -4
- package/docs/faq.md +15 -7
- package/docs/getting-started.md +8 -5
- package/docs/manual-legacy.html +380 -0
- package/docs/manual.html +126 -380
- package/docs/roadmap.md +7 -8
- package/hooks/commit-msg +39 -22
- package/lib/adaptive.d.ts +15 -0
- package/lib/adaptive.js +54 -0
- package/lib/adaptive.js.map +1 -0
- package/lib/client.js +719 -433
- package/lib/client.js.map +3 -3
- package/lib/controller.d.ts +50 -0
- package/lib/controller.js +132 -0
- package/lib/controller.js.map +1 -1
- package/lib/dev-task.js +413 -18
- package/lib/dev-task.js.map +1 -1
- package/lib/engine.d.ts +20 -0
- package/lib/engine.js +4 -1
- package/lib/engine.js.map +1 -1
- package/lib/hook.js +50 -29
- package/lib/hook.js.map +1 -1
- package/lib/project-init.d.ts +23 -0
- package/lib/project-init.js +94 -0
- package/lib/project-init.js.map +1 -0
- package/lib/skill-audit.js +8 -7
- package/lib/skill-audit.js.map +1 -1
- package/lib/sonar.d.ts +33 -0
- package/lib/sonar.js +80 -0
- package/lib/sonar.js.map +1 -0
- package/package.json +5 -4
- package/preset/enable.mjs +2 -2
- package/preset/persona.md +4 -2
- package/scripts/verify-package.mjs +7 -3
- package/skills/architecture-design/SKILL.md +11 -0
- package/skills/code-development/SKILL.md +11 -0
- package/skills/code-review/SKILL.md +11 -0
- package/skills/eng-delivery/SKILL.md +6 -2
- package/skills/requirements-analysis/SKILL.md +11 -0
- package/skills/task-orchestration/SKILL.md +11 -0
- package/skills/test-validation/SKILL.md +11 -0
package/lib/dev-task.js
CHANGED
|
@@ -17,6 +17,9 @@ import { fileURLToPath } from 'node:url';
|
|
|
17
17
|
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
|
+
import { ADAPTIVE_VERSION, COMPLEXITY_OPTIONS, META_STAGES, adaptiveWorkflow, isComplexity, metaForStage } from "./adaptive.js";
|
|
21
|
+
import { inspectSonar } from "./sonar.js";
|
|
22
|
+
import { scanProject } from "./project-init.js";
|
|
20
23
|
import { hashConfig, hashText } from "./snapshot.js";
|
|
21
24
|
import { withUserSkillProfiles } from "./user-skill-profiles.js";
|
|
22
25
|
import { loadedSkills, needsSkillReceipt, obligationStages, skillBlockers } from "./skill-audit.js";
|
|
@@ -397,6 +400,140 @@ async function readSkillAt(ref, fs, cwd) {
|
|
|
397
400
|
return read(join(dshHome(), 'skills', safe, 'SKILL.md'));
|
|
398
401
|
}
|
|
399
402
|
}
|
|
403
|
+
const RESOURCE_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
404
|
+
const META_EVIDENCE = {
|
|
405
|
+
'requirements-analysis': 'artifact',
|
|
406
|
+
'architecture-design': 'artifact',
|
|
407
|
+
'task-orchestration': 'artifact',
|
|
408
|
+
'code-development': 'none',
|
|
409
|
+
'test-validation': 'none',
|
|
410
|
+
'code-review': 'review',
|
|
411
|
+
};
|
|
412
|
+
async function readAdaptiveConfig(fs, cwd) {
|
|
413
|
+
const raw = await readText(fs, '.dsh/meta.json', cwd);
|
|
414
|
+
if (raw === undefined)
|
|
415
|
+
return {};
|
|
416
|
+
let config;
|
|
417
|
+
try {
|
|
418
|
+
config = JSON.parse(raw);
|
|
419
|
+
}
|
|
420
|
+
catch (error) {
|
|
421
|
+
throw new Error(`invalid .dsh/meta.json: ${error instanceof Error ? error.message : String(error)}`);
|
|
422
|
+
}
|
|
423
|
+
if (config === null || typeof config !== 'object' || Array.isArray(config))
|
|
424
|
+
throw new Error('.dsh/meta.json must be an object');
|
|
425
|
+
if (config.meta_bindings !== undefined) {
|
|
426
|
+
if (config.meta_bindings === null || typeof config.meta_bindings !== 'object' || Array.isArray(config.meta_bindings)) {
|
|
427
|
+
throw new Error('.dsh/meta.json meta_bindings must be an object');
|
|
428
|
+
}
|
|
429
|
+
for (const [meta, names] of Object.entries(config.meta_bindings)) {
|
|
430
|
+
if (!(meta in META_STAGES))
|
|
431
|
+
throw new Error(`unknown meta-skill "${meta}" in .dsh/meta.json`);
|
|
432
|
+
if (!Array.isArray(names) || !names.every(name => typeof name === 'string' && RESOURCE_NAME.test(name))) {
|
|
433
|
+
throw new Error(`meta_bindings.${meta} must be an array of simple Skill names`);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
if (config.sonar !== undefined) {
|
|
438
|
+
if (config.sonar === null || typeof config.sonar !== 'object' || Array.isArray(config.sonar)) {
|
|
439
|
+
throw new Error('.dsh/meta.json sonar must be an object');
|
|
440
|
+
}
|
|
441
|
+
if (config.sonar.enabled !== undefined && typeof config.sonar.enabled !== 'boolean') {
|
|
442
|
+
throw new Error('.dsh/meta.json sonar.enabled must be boolean');
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return config;
|
|
446
|
+
}
|
|
447
|
+
function sonarPolicyFor(project) {
|
|
448
|
+
const sonar = project.sonar;
|
|
449
|
+
if (sonar?.enabled !== true)
|
|
450
|
+
return undefined;
|
|
451
|
+
const host_url = sonar.host_url ?? process.env.SONAR_HOST_URL;
|
|
452
|
+
const project_key = sonar.project_key ?? process.env.SONAR_PROJECT_KEY;
|
|
453
|
+
const token_env = sonar.token_env ?? 'SONAR_TOKEN';
|
|
454
|
+
if (!host_url || !/^https?:\/\//u.test(host_url) || !project_key?.trim()
|
|
455
|
+
|| !/^[A-Z][A-Z0-9_]*$/u.test(token_env)) {
|
|
456
|
+
throw new Error('enabled SonarQube needs host_url, project_key, and a valid token_env in .dsh/meta.json or environment');
|
|
457
|
+
}
|
|
458
|
+
const parsed = new URL(host_url);
|
|
459
|
+
if (parsed.username || parsed.password || parsed.search || parsed.hash) {
|
|
460
|
+
throw new Error('SonarQube host_url must not contain credentials, a query, or a fragment');
|
|
461
|
+
}
|
|
462
|
+
const mode = sonar.mode ?? 'branch';
|
|
463
|
+
if (mode !== 'branch' && mode !== 'pull-request')
|
|
464
|
+
throw new Error('sonar.mode must be branch or pull-request');
|
|
465
|
+
return { enabled: true, host_url, project_key, token_env, mode };
|
|
466
|
+
}
|
|
467
|
+
async function resolveLogicalSkill(name, fs, cwd) {
|
|
468
|
+
if (!RESOURCE_NAME.test(name))
|
|
469
|
+
throw new Error(`invalid Skill name "${name}"`);
|
|
470
|
+
for (const source of ['project', 'codex-project', 'user', 'bundled']) {
|
|
471
|
+
const ref = { source, name };
|
|
472
|
+
if (await readSkillAt(ref, fs, cwd) !== undefined)
|
|
473
|
+
return ref;
|
|
474
|
+
}
|
|
475
|
+
throw new Error(`Skill "${name}" was not found in project, user or bundled sources`);
|
|
476
|
+
}
|
|
477
|
+
async function profileFor(ref, fs, cwd) {
|
|
478
|
+
let raw;
|
|
479
|
+
switch (ref.source) {
|
|
480
|
+
case 'project':
|
|
481
|
+
raw = await readText(fs, `.dsh/skills/${ref.name}/profile.json`, cwd);
|
|
482
|
+
break;
|
|
483
|
+
case 'codex-project':
|
|
484
|
+
raw = await readText(fs, `.agents/skills/${ref.name}/profile.json`, cwd);
|
|
485
|
+
break;
|
|
486
|
+
case 'user':
|
|
487
|
+
raw = readAbsRule(join(dshHome(), 'skills', ref.name, 'profile.json'));
|
|
488
|
+
break;
|
|
489
|
+
case 'bundled':
|
|
490
|
+
raw = readAbsRule(join(BUNDLED_SKILLS_DIR, ref.name, 'profile.json'));
|
|
491
|
+
break;
|
|
492
|
+
}
|
|
493
|
+
if (raw === undefined)
|
|
494
|
+
return undefined;
|
|
495
|
+
let value;
|
|
496
|
+
try {
|
|
497
|
+
value = JSON.parse(raw);
|
|
498
|
+
}
|
|
499
|
+
catch (error) {
|
|
500
|
+
throw new Error(`invalid profile for ${formatResourceRef(ref)}: ${error instanceof Error ? error.message : String(error)}`);
|
|
501
|
+
}
|
|
502
|
+
if (!value || !Array.isArray(value.rules) || !value.rules.every(rule => rule && RESOURCE_NAME.test(rule.name) && ['bundled', 'project', 'user'].includes(rule.source))
|
|
503
|
+
|| (value.evidence !== undefined && !['command', 'artifact', 'review', 'manual', 'none'].includes(value.evidence))) {
|
|
504
|
+
throw new Error(`invalid profile for ${formatResourceRef(ref)}`);
|
|
505
|
+
}
|
|
506
|
+
return value;
|
|
507
|
+
}
|
|
508
|
+
/** Resolve all bound methods once at task creation; references stay source-qualified afterwards. */
|
|
509
|
+
async function resolveAdaptiveWorkflow(grade, fs, cwd, selected) {
|
|
510
|
+
const config = adaptiveWorkflow(grade);
|
|
511
|
+
const project = selected ?? await readAdaptiveConfig(fs, cwd);
|
|
512
|
+
const stage_bindings = {};
|
|
513
|
+
const skill_profiles = {};
|
|
514
|
+
for (const stage of config.stages) {
|
|
515
|
+
const meta = metaForStage(stage);
|
|
516
|
+
if (meta === undefined)
|
|
517
|
+
continue;
|
|
518
|
+
const names = [...new Set([meta, ...(project.meta_bindings?.[meta] ?? [])])];
|
|
519
|
+
const skills = [];
|
|
520
|
+
for (const name of names) {
|
|
521
|
+
const ref = await resolveLogicalSkill(name, fs, cwd);
|
|
522
|
+
const profile = await profileFor(ref, fs, cwd) ?? {
|
|
523
|
+
rules: [], evidence: name === meta ? META_EVIDENCE[meta] : 'none',
|
|
524
|
+
};
|
|
525
|
+
skill_profiles[formatResourceRef(ref)] = profile;
|
|
526
|
+
skills.push({ skill: ref, rules: profile.rules, evidence: profile.evidence ?? 'none' });
|
|
527
|
+
}
|
|
528
|
+
stage_bindings[stage] = { skills };
|
|
529
|
+
}
|
|
530
|
+
config.stage_bindings = stage_bindings;
|
|
531
|
+
config.skill_profiles = skill_profiles;
|
|
532
|
+
const problems = validateWorkflow(config);
|
|
533
|
+
if (problems.length > 0)
|
|
534
|
+
throw new Error(`invalid adaptive workflow:\n- ${problems.join('\n- ')}`);
|
|
535
|
+
return config;
|
|
536
|
+
}
|
|
400
537
|
/**
|
|
401
538
|
* Capture every skill and rule body the config resolves to at task creation.
|
|
402
539
|
*
|
|
@@ -613,7 +750,7 @@ async function renderBindings(stage, workflow, fs, cwd, frozen) {
|
|
|
613
750
|
return '';
|
|
614
751
|
const { resolved: rules, missing, legacy } = await rulesForBinding(binding, fs, cwd, frozen);
|
|
615
752
|
const skillPart = skills.length > 0
|
|
616
|
-
? `skills to load: ${skills.map(skill => formatResourceRef(skill.skill)).join(', ')} (use
|
|
753
|
+
? `skills to load: ${skills.map(skill => formatResourceRef(skill.skill)).join(', ')} (use dev_task operation=load_skill with the exact source:name)`
|
|
617
754
|
: 'skills to load: none';
|
|
618
755
|
const currentSkills = await skillBodiesForBinding(binding, fs, cwd);
|
|
619
756
|
const skillInstructions = currentSkills.length > 0
|
|
@@ -674,7 +811,7 @@ function normalizeItems(items, previous = []) {
|
|
|
674
811
|
/** Hash declared deliverables through host FS; task bookkeeping is excluded to avoid self-invalidation. */
|
|
675
812
|
async function scopeFingerprint(fs, state, cwd) {
|
|
676
813
|
const entries = [];
|
|
677
|
-
for (const path of [...new Set(state.files)].filter(path => !/^\.dsh\/(task-[^/]+\.json|eng\.json)$/.test(path)).sort()) {
|
|
814
|
+
for (const path of [...new Set(state.files)].filter(path => !/^\.dsh\/(task-[^/]+\.json|eng\.json|meta\.json)$/.test(path)).sort()) {
|
|
678
815
|
assertInsideRoot(state.root ?? cwd ?? '', state.root ?? cwd ?? '', path);
|
|
679
816
|
const root = state.root ?? cwd;
|
|
680
817
|
const target = await fs.resolve(path, root === undefined ? undefined : { cwd: root });
|
|
@@ -723,10 +860,55 @@ async function evidenceBlockers(fs, state, workflow, cwd) {
|
|
|
723
860
|
return blockers;
|
|
724
861
|
}
|
|
725
862
|
async function assertFreshEvidence(fs, state, workflow, cwd) {
|
|
726
|
-
const blockers = await evidenceBlockers(fs, state, workflow, cwd);
|
|
863
|
+
const blockers = [...await evidenceBlockers(fs, state, workflow, cwd), ...await sonarBlockers(fs, state, cwd)];
|
|
727
864
|
if (blockers.length)
|
|
728
865
|
throw new Error(blockers.join('; '));
|
|
729
866
|
}
|
|
867
|
+
function validateInitResources(resources) {
|
|
868
|
+
if (!Array.isArray(resources) || resources.length === 0 || resources.length > 24) {
|
|
869
|
+
throw new Error('init_project requires 1–24 Skill/Rule resources');
|
|
870
|
+
}
|
|
871
|
+
const names = new Set();
|
|
872
|
+
for (const resource of resources) {
|
|
873
|
+
if (!resource || !['skill', 'rule'].includes(resource.kind) || !RESOURCE_NAME.test(resource.name)
|
|
874
|
+
|| typeof resource.content !== 'string' || !resource.content.trim() || resource.content.length > 16_000) {
|
|
875
|
+
throw new Error('each init_project resource needs kind, kebab-case name, and nonempty content under 16,000 characters');
|
|
876
|
+
}
|
|
877
|
+
const key = `${resource.kind}:${resource.name}`;
|
|
878
|
+
if (names.has(key))
|
|
879
|
+
throw new Error(`duplicate init_project resource ${key}`);
|
|
880
|
+
names.add(key);
|
|
881
|
+
if (resource.kind === 'skill') {
|
|
882
|
+
if (typeof resource.description !== 'string' || !resource.description.trim())
|
|
883
|
+
throw new Error(`Skill ${resource.name} needs a description`);
|
|
884
|
+
if (!Array.isArray(resource.meta_skills) || resource.meta_skills.length === 0 ||
|
|
885
|
+
!resource.meta_skills.every(meta => meta in META_STAGES))
|
|
886
|
+
throw new Error(`Skill ${resource.name} needs valid meta_skills`);
|
|
887
|
+
if (resource.rules !== undefined && (!Array.isArray(resource.rules) || !resource.rules.every(rule => RESOURCE_NAME.test(rule)))) {
|
|
888
|
+
throw new Error(`Skill ${resource.name} has invalid rule names`);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
return resources;
|
|
893
|
+
}
|
|
894
|
+
async function sonarBlockers(fs, state, cwd) {
|
|
895
|
+
if (state.sonar_policy === undefined || !['代码审核', '完成'].includes(state.stage))
|
|
896
|
+
return [];
|
|
897
|
+
if (state.sonar_audit === undefined)
|
|
898
|
+
return ['SonarQube audit is enabled but has not been run; call sonar_check in code review'];
|
|
899
|
+
const current = await scopeFingerprint(fs, state, cwd);
|
|
900
|
+
const problems = [];
|
|
901
|
+
if (state.sonar_audit.scope_hash !== current)
|
|
902
|
+
problems.push('SonarQube audit is stale after file changes; rerun CI scan and sonar_check');
|
|
903
|
+
const commit = state.commits.findLast(entry => entry.hash !== undefined)?.hash;
|
|
904
|
+
if (!commit || state.sonar_audit.commit_hash !== commit)
|
|
905
|
+
problems.push('SonarQube audit does not match the task\'s latest recorded commit');
|
|
906
|
+
if (state.sonar_audit.gate !== 'OK')
|
|
907
|
+
problems.push(`SonarQube Quality Gate is ${state.sonar_audit.gate}`);
|
|
908
|
+
if (state.sonar_audit.blocking.length)
|
|
909
|
+
problems.push(`${state.sonar_audit.blocking.length} medium/high SonarQube new-code findings remain`);
|
|
910
|
+
return problems;
|
|
911
|
+
}
|
|
730
912
|
/** Resolve one item by id for the item-scoped audit operations. */
|
|
731
913
|
function requireItem(state, id) {
|
|
732
914
|
if (id === undefined || id === '')
|
|
@@ -772,7 +954,7 @@ export async function approveAdvance(ctx, state, target, workflow, confirmations
|
|
|
772
954
|
state.solution_confirmed = true;
|
|
773
955
|
return assertAdvance(state, target, workflow);
|
|
774
956
|
}
|
|
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'];
|
|
957
|
+
const OPERATIONS = ['status', 'assess', 'create', 'load_skill', 'record', 'items', 'scope', 'dispatch', 'review_item', 'advance', 'verify', 'sonar_check', 'learn_rule', 'review', 'commit', 'complete', 'revise', 'config', 'install_hook', 'verify_hook', 'init', 'init_project', 'set_risk', 'skill_result'];
|
|
776
958
|
const TOOL_DESCRIPTION = 'Own the engineering delivery workflow as hard state. Read or create the task record, record a ' +
|
|
777
959
|
'stage artifact, record each implementation item\'s subagent dispatch and two-stage (spec + quality) ' +
|
|
778
960
|
'review audit (the flow decides how many verdicts per item), advance one stage (rejected unless every configured guard already holds, including ' +
|
|
@@ -801,6 +983,19 @@ export function registerDevTask(ctx) {
|
|
|
801
983
|
branch: { type: 'string', description: 'Current git branch (recorded on create).' },
|
|
802
984
|
title: { type: 'string', description: 'Task title (create).' },
|
|
803
985
|
work_size: { type: 'string', enum: ['tiny', 'standard', 'complex'], description: 'Workload tier (create).' },
|
|
986
|
+
complexity: { type: 'string', enum: ['low', 'medium', 'high', 'ultra'], description: 'Task-level complexity (assess/create). Select from requirement scope and implementation dependencies; independent of risk_level. Passing this on create selects the adaptive meta-skill flow.' },
|
|
987
|
+
complexity_reason: { type: 'string', description: 'Evidence-based reason for the selected complexity (create).' },
|
|
988
|
+
ce_task_id: { type: 'string', description: 'SonarQube Compute Engine task id from the completed CI scan report-task.txt (sonar_check).' },
|
|
989
|
+
pull_request: { type: 'string', description: 'GitLab merge request IID or PR number when SonarQube mode is pull-request (sonar_check).' },
|
|
990
|
+
issue_key: { type: 'string', description: 'A blocking SonarQube issue key from this task\'s audit history (learn_rule).' },
|
|
991
|
+
rule_name: { type: 'string', description: 'New project Rule name (learn_rule).' },
|
|
992
|
+
learning_reason: { type: 'string', description: 'Why this failure generalizes beyond the current line of code (learn_rule).' },
|
|
993
|
+
resources: { type: 'array', description: 'Project Skill/Rule drafts for init_project propose/apply; content is reviewed before creation.',
|
|
994
|
+
items: { type: 'object', additionalProperties: false, properties: {
|
|
995
|
+
kind: { type: 'string', enum: ['skill', 'rule'] }, name: { type: 'string' }, description: { type: 'string' },
|
|
996
|
+
content: { type: 'string' }, meta_skills: { type: 'array', items: { type: 'string' } },
|
|
997
|
+
rules: { type: 'array', items: { type: 'string' } },
|
|
998
|
+
} } },
|
|
804
999
|
risk_level: { type: 'string', enum: ['standard', 'high_risk'], description: 'Risk tier (create).' },
|
|
805
1000
|
items: {
|
|
806
1001
|
type: 'array',
|
|
@@ -870,7 +1065,8 @@ export function registerDevTask(ctx) {
|
|
|
870
1065
|
continue;
|
|
871
1066
|
const task = JSON.parse(raw);
|
|
872
1067
|
if (!a.branch || task.branch === a.branch)
|
|
873
|
-
tasks.push({ id: task.id, title: task.title, branch: task.branch, stage: task.stage
|
|
1068
|
+
tasks.push({ id: task.id, title: task.title, branch: task.branch, stage: task.stage,
|
|
1069
|
+
...(task.complexity !== undefined ? { complexity: task.complexity } : {}) });
|
|
874
1070
|
}
|
|
875
1071
|
return JSON.stringify({ tasks, next_action: tasks.length ? 'Call status with the selected task_id; create a new task for a new requirement.' : 'No task found. Call create with task_id, title and current branch.' }, null, 2);
|
|
876
1072
|
}
|
|
@@ -881,8 +1077,22 @@ export function registerDevTask(ctx) {
|
|
|
881
1077
|
valid: resolved.problems.length === 0,
|
|
882
1078
|
problems: resolved.problems,
|
|
883
1079
|
workflow: resolved.config,
|
|
1080
|
+
adaptive: { grades: COMPLEXITY_OPTIONS, config_path: '.dsh/meta.json', task_selected: true },
|
|
884
1081
|
}, null, 2);
|
|
885
1082
|
}
|
|
1083
|
+
if (a.operation === 'assess') {
|
|
1084
|
+
if (a.complexity === undefined) {
|
|
1085
|
+
return JSON.stringify({ grades: COMPLEXITY_OPTIONS,
|
|
1086
|
+
instruction: 'Analyze the requirement, choose one grade, then call assess with complexity to preview its stages and effective Skill sources. Record the reason on create. Risk is assessed separately.' }, null, 2);
|
|
1087
|
+
}
|
|
1088
|
+
if (!isComplexity(a.complexity))
|
|
1089
|
+
throw new Error(`unknown complexity "${String(a.complexity)}"`);
|
|
1090
|
+
const config = await resolveAdaptiveWorkflow(a.complexity, fs, cwd);
|
|
1091
|
+
return JSON.stringify({ complexity: a.complexity, stages: config.stages,
|
|
1092
|
+
meta_skills: Object.fromEntries(Object.entries(config.stage_bindings ?? {}).map(([stage, binding]) => [stage, (binding.skills ?? []).map(entry => ({ logical_name: entry.skill.name, effective_source: entry.skill.source,
|
|
1093
|
+
rules: entry.rules }))])),
|
|
1094
|
+
artifacts: config.artifacts, review_depth: config.review_depth }, null, 2);
|
|
1095
|
+
}
|
|
886
1096
|
if (a.operation === 'install_hook') {
|
|
887
1097
|
await installCommitHook(fs, cwd, await resolveWriteMode(ctx, a, exec));
|
|
888
1098
|
return 'installed commit-msg hook at .git/hooks/commit-msg — git commit is now gated by the task state';
|
|
@@ -897,6 +1107,72 @@ export function registerDevTask(ctx) {
|
|
|
897
1107
|
}
|
|
898
1108
|
throw new Error('hook integrity FAILED — .git/hooks/commit-msg differs from the bundled gate; reinstall with install_hook');
|
|
899
1109
|
}
|
|
1110
|
+
if (a.operation === 'init_project') {
|
|
1111
|
+
const root = await detectRoot(projectProbe(fs), cwd ?? '');
|
|
1112
|
+
if (!cwd || resolve(root) !== resolve(cwd)) {
|
|
1113
|
+
throw new Error('init_project must run with the project root as the session workspace');
|
|
1114
|
+
}
|
|
1115
|
+
const phase = a.phase ?? 'inspect';
|
|
1116
|
+
const inventory = await scanProject(projectProbe(fs), root);
|
|
1117
|
+
if (phase === 'inspect')
|
|
1118
|
+
return JSON.stringify(inventory, null, 2);
|
|
1119
|
+
if (phase !== 'propose' && phase !== 'apply')
|
|
1120
|
+
throw new Error('init_project phase must be inspect, propose, or apply');
|
|
1121
|
+
const resources = validateInitResources(a.resources);
|
|
1122
|
+
const metaRaw = await readText(fs, '.dsh/meta.json', cwd);
|
|
1123
|
+
const meta = await readAdaptiveConfig(fs, cwd);
|
|
1124
|
+
const createdRules = new Set(resources.filter(resource => resource.kind === 'rule').map(resource => resource.name));
|
|
1125
|
+
const paths = [];
|
|
1126
|
+
for (const resource of resources) {
|
|
1127
|
+
const path = resource.kind === 'rule' ? `.dsh/rules/${resource.name}.md` : `.dsh/skills/${resource.name}/SKILL.md`;
|
|
1128
|
+
assertInsideRoot(root, cwd, path);
|
|
1129
|
+
if (await readText(fs, path, cwd) !== undefined)
|
|
1130
|
+
throw new Error(`init_project will not overwrite ${path}`);
|
|
1131
|
+
paths.push(path);
|
|
1132
|
+
if (resource.kind === 'skill') {
|
|
1133
|
+
const profilePath = `.dsh/skills/${resource.name}/profile.json`;
|
|
1134
|
+
if (await readText(fs, profilePath, cwd) !== undefined)
|
|
1135
|
+
throw new Error(`init_project will not overwrite ${profilePath}`);
|
|
1136
|
+
paths.push(profilePath);
|
|
1137
|
+
for (const rule of resource.rules ?? []) {
|
|
1138
|
+
if (!createdRules.has(rule) && await readText(fs, `.dsh/rules/${rule}.md`, cwd) === undefined) {
|
|
1139
|
+
throw new Error(`Skill ${resource.name} references missing project Rule ${rule}`);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
const metaHash = hashText(metaRaw ?? '');
|
|
1145
|
+
const proposalHash = hashText(JSON.stringify(resources) + '\n' + metaHash);
|
|
1146
|
+
if (phase === 'propose')
|
|
1147
|
+
return JSON.stringify({ inventory, resources, files_to_create: paths,
|
|
1148
|
+
meta_config: '.dsh/meta.json', existing_hash: metaHash, expected_hash: proposalHash,
|
|
1149
|
+
instruction: 'Review each project-specific claim against the listed evidence. Apply with identical resources and expected_hash; existing files are protected.' }, null, 2);
|
|
1150
|
+
if (a.expected_hash !== proposalHash || a.existing_hash !== metaHash) {
|
|
1151
|
+
throw new Error('init_project draft or meta config changed since propose; inspect and propose again');
|
|
1152
|
+
}
|
|
1153
|
+
const writeMode = await resolveWriteMode(ctx, a, exec);
|
|
1154
|
+
// Publish rules first, then Skills/profiles, then the binding map. A failed final
|
|
1155
|
+
// write can leave unbound resources, but never a live binding to an absent Rule.
|
|
1156
|
+
for (const resource of resources.filter(resource => resource.kind === 'rule')) {
|
|
1157
|
+
await writeText(fs, `.dsh/rules/${resource.name}.md`, resource.content.trimEnd() + '\n', cwd, writeMode, { kind: 'createIfAbsent' });
|
|
1158
|
+
}
|
|
1159
|
+
for (const resource of resources.filter(resource => resource.kind === 'skill')) {
|
|
1160
|
+
const description = resource.description.replace(/[\r\n]+/gu, ' ').trim();
|
|
1161
|
+
const body = `---\nname: ${resource.name}\ndescription: ${description}\n---\n\n${resource.content.trimEnd()}\n`;
|
|
1162
|
+
await writeText(fs, `.dsh/skills/${resource.name}/SKILL.md`, body, cwd, writeMode, { kind: 'createIfAbsent' });
|
|
1163
|
+
const profile = { rules: (resource.rules ?? []).map(name => ({ source: 'project', name })), evidence: 'none' };
|
|
1164
|
+
await writeText(fs, `.dsh/skills/${resource.name}/profile.json`, JSON.stringify(profile, null, 2) + '\n', cwd, writeMode, { kind: 'createIfAbsent' });
|
|
1165
|
+
}
|
|
1166
|
+
const bindings = { ...meta.meta_bindings };
|
|
1167
|
+
for (const resource of resources.filter(resource => resource.kind === 'skill')) {
|
|
1168
|
+
for (const skill of resource.meta_skills ?? []) {
|
|
1169
|
+
bindings[skill] = [...new Set([...(bindings[skill] ?? []), resource.name])];
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
const info = await fs.lstat('.dsh/meta.json', { cwd });
|
|
1173
|
+
await writeText(fs, '.dsh/meta.json', JSON.stringify({ ...meta, meta_bindings: bindings }, null, 2) + '\n', cwd, writeMode, info === undefined ? { kind: 'createIfAbsent' } : { kind: 'replaceIfVersion', version: info.version });
|
|
1174
|
+
return JSON.stringify({ created: paths, bindings, next_action: 'Call assess to inspect the effective project Skills before creating a task.' }, null, 2);
|
|
1175
|
+
}
|
|
900
1176
|
if (a.operation === 'init') {
|
|
901
1177
|
const phase = a.phase ?? 'inspect';
|
|
902
1178
|
const existing = await readText(fs, 'AGENTS.md', cwd);
|
|
@@ -976,12 +1252,21 @@ export function registerDevTask(ctx) {
|
|
|
976
1252
|
if (!a.task_id || !a.title || !a.branch) {
|
|
977
1253
|
throw new Error('create requires task_id, title, and branch');
|
|
978
1254
|
}
|
|
979
|
-
|
|
1255
|
+
if (a.complexity !== undefined && !isComplexity(a.complexity))
|
|
1256
|
+
throw new Error(`unknown complexity "${String(a.complexity)}"`);
|
|
1257
|
+
if (a.complexity !== undefined && !a.complexity_reason?.trim()) {
|
|
1258
|
+
throw new Error('adaptive create requires complexity_reason explaining the requirement evidence for this grade');
|
|
1259
|
+
}
|
|
1260
|
+
const adaptiveConfig = a.complexity === undefined ? undefined : await readAdaptiveConfig(fs, cwd);
|
|
1261
|
+
const resolved = a.complexity !== undefined
|
|
1262
|
+
? { flow: `adaptive-${a.complexity}`, version: ADAPTIVE_VERSION,
|
|
1263
|
+
config: await resolveAdaptiveWorkflow(a.complexity, fs, cwd, adaptiveConfig), problems: [] }
|
|
1264
|
+
: await resolveWorkflow(fs, cwd);
|
|
980
1265
|
if (resolved.problems.length > 0) {
|
|
981
1266
|
throw new Error(`invalid .dsh/eng.json — fix the project config first:\n- ${resolved.problems.join('\n- ')}`);
|
|
982
1267
|
}
|
|
983
1268
|
const risk = a.risk_level ?? 'standard';
|
|
984
|
-
if (risk === 'high_risk' && !flowSatisfies(resolved.flow, HIGH_RISK_REQUIRED_CAPABILITIES)) {
|
|
1269
|
+
if (risk === 'high_risk' && a.complexity === undefined && !flowSatisfies(resolved.flow, HIGH_RISK_REQUIRED_CAPABILITIES)) {
|
|
985
1270
|
throw new Error(`high_risk task cannot run on flow "${resolved.flow}" — it lacks the required capabilities ` +
|
|
986
1271
|
`(${HIGH_RISK_REQUIRED_CAPABILITIES.join(', ')}). Use the standard flow or lower the task risk.`);
|
|
987
1272
|
}
|
|
@@ -1000,12 +1285,15 @@ export function registerDevTask(ctx) {
|
|
|
1000
1285
|
resources: frozen.resources,
|
|
1001
1286
|
};
|
|
1002
1287
|
const root = await detectRoot(projectProbe(fs), cwd ?? '');
|
|
1288
|
+
const sonarPolicy = adaptiveConfig === undefined ? undefined : sonarPolicyFor(adaptiveConfig);
|
|
1003
1289
|
const state = newTask({
|
|
1004
1290
|
id: a.task_id,
|
|
1005
1291
|
title: a.title,
|
|
1006
1292
|
branch: a.branch,
|
|
1007
1293
|
work_size: a.work_size ?? 'standard',
|
|
1008
1294
|
risk_level: risk,
|
|
1295
|
+
...(a.complexity !== undefined ? { complexity: a.complexity, complexity_reason: a.complexity_reason.trim() } : {}),
|
|
1296
|
+
...(sonarPolicy !== undefined ? { sonar_policy: sonarPolicy } : {}),
|
|
1009
1297
|
flow,
|
|
1010
1298
|
root,
|
|
1011
1299
|
project_type: await detectType(projectProbe(fs), root),
|
|
@@ -1015,7 +1303,7 @@ export function registerDevTask(ctx) {
|
|
|
1015
1303
|
state.files = a.files ?? [];
|
|
1016
1304
|
assertInsideRoot(root, cwd ?? '', taskPath(state.id));
|
|
1017
1305
|
await writeTask(fs, state, cwd, await resolveWriteMode(ctx, a, exec));
|
|
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.`;
|
|
1306
|
+
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${state.complexity !== undefined ? ' with dev_task load_skill and exact source:name' : ''}. Record each skill's declared evidence. Terminal bindings must finish before entering the terminal stage.`;
|
|
1019
1307
|
}
|
|
1020
1308
|
if (!a.task_id)
|
|
1021
1309
|
throw new Error('task_id is required for this operation');
|
|
@@ -1024,7 +1312,7 @@ export function registerDevTask(ctx) {
|
|
|
1024
1312
|
if (state.completed !== undefined && a.operation === 'complete') {
|
|
1025
1313
|
return `task already completed at ${state.completed.at}`;
|
|
1026
1314
|
}
|
|
1027
|
-
if (state.completed !== undefined && a.operation !== 'status' && a.operation !== 'revise') {
|
|
1315
|
+
if (state.completed !== undefined && a.operation !== 'status' && a.operation !== 'revise' && a.operation !== 'learn_rule') {
|
|
1028
1316
|
throw new Error('task is completed; use revise to reopen it before changing task state');
|
|
1029
1317
|
}
|
|
1030
1318
|
if (a.operation === 'load_skill') {
|
|
@@ -1033,8 +1321,8 @@ export function registerDevTask(ctx) {
|
|
|
1033
1321
|
throw new Error(`stage "${stage}" is not currently actionable`);
|
|
1034
1322
|
const entry = (workflow.stage_bindings?.[stage]?.skills ?? [])
|
|
1035
1323
|
.find(binding => formatResourceRef(binding.skill) === a.skill_name);
|
|
1036
|
-
if (!entry
|
|
1037
|
-
throw new Error('load_skill requires
|
|
1324
|
+
if (!entry)
|
|
1325
|
+
throw new Error('load_skill requires an exact source:name bound at the current or upcoming terminal stage');
|
|
1038
1326
|
const content = await readSkillAt(entry.skill, fs, cwd);
|
|
1039
1327
|
if (content === undefined)
|
|
1040
1328
|
throw new Error(`bound skill ${a.skill_name} cannot be read`);
|
|
@@ -1061,13 +1349,15 @@ export function registerDevTask(ctx) {
|
|
|
1061
1349
|
const completionIssues = state.completed === undefined
|
|
1062
1350
|
? [...completionBlockers(state, workflow), ...missingSkills,
|
|
1063
1351
|
...skillResourceBlockers(missingSkillFiles, state.stage),
|
|
1064
|
-
...resourceBlockers(missingRules, state.stage), ...staleEvidence]
|
|
1352
|
+
...resourceBlockers(missingRules, state.stage), ...staleEvidence, ...await sonarBlockers(fs, state, cwd)]
|
|
1065
1353
|
: [];
|
|
1066
1354
|
return JSON.stringify({
|
|
1067
1355
|
id: state.id,
|
|
1068
1356
|
stage: state.stage,
|
|
1069
1357
|
flow: state.flow !== undefined ? { flow: state.flow.flow, version: state.flow.version } : null,
|
|
1070
1358
|
risk_level: state.risk_level,
|
|
1359
|
+
complexity: state.complexity ?? null,
|
|
1360
|
+
complexity_reason: state.complexity_reason ?? null,
|
|
1071
1361
|
completed: state.completed ?? null,
|
|
1072
1362
|
completion_blockers: completionIssues,
|
|
1073
1363
|
work_size: state.work_size,
|
|
@@ -1077,12 +1367,14 @@ export function registerDevTask(ctx) {
|
|
|
1077
1367
|
items: state.items,
|
|
1078
1368
|
skill_obligations: obligationStages(state, workflow).map(stage => {
|
|
1079
1369
|
const bindings = workflow.stage_bindings?.[stage]?.skills ?? [];
|
|
1080
|
-
const skills = bindings.map(entry => entry.skill.source === 'codex-project'
|
|
1370
|
+
const skills = bindings.map(entry => state.complexity !== undefined || entry.skill.source === 'codex-project'
|
|
1371
|
+
? formatResourceRef(entry.skill) : entry.skill.name);
|
|
1081
1372
|
// Only bindings whose evidence is a command owe a command receipt, so a
|
|
1082
1373
|
// document-producing skill is not told to run something irrelevant.
|
|
1083
1374
|
const command_receipts_required = bindings
|
|
1084
1375
|
.filter(entry => entry.evidence === undefined || entry.evidence === 'command')
|
|
1085
|
-
.map(entry => entry.skill.source === 'codex-project'
|
|
1376
|
+
.map(entry => state.complexity !== undefined || entry.skill.source === 'codex-project'
|
|
1377
|
+
? formatResourceRef(entry.skill) : entry.skill.name)
|
|
1086
1378
|
.filter(name => needsSkillReceipt(name));
|
|
1087
1379
|
return { stage, skills, command_receipts_required };
|
|
1088
1380
|
}),
|
|
@@ -1108,6 +1400,12 @@ export function registerDevTask(ctx) {
|
|
|
1108
1400
|
commits: state.commits,
|
|
1109
1401
|
verification: state.verification,
|
|
1110
1402
|
review: state.review,
|
|
1403
|
+
sonar: state.sonar_policy === undefined ? { enabled: false } : {
|
|
1404
|
+
enabled: true, mode: state.sonar_policy.mode, project_key: state.sonar_policy.project_key,
|
|
1405
|
+
audit: state.sonar_audit ?? null, blockers: await sonarBlockers(fs, state, cwd),
|
|
1406
|
+
prior_audits: state.sonar_history?.length ?? 0,
|
|
1407
|
+
},
|
|
1408
|
+
learned_rules: state.learned_rules ?? [],
|
|
1111
1409
|
artifact_requirements: workflow.artifacts.filter(def => def.stage === state.stage).map(def => ({
|
|
1112
1410
|
id: def.id, name: def.name, fields: def.fields,
|
|
1113
1411
|
missing_fields: def.fields.filter(field => !(state.artifacts[def.id]?.[field] ?? '').trim()),
|
|
@@ -1130,6 +1428,91 @@ export function registerDevTask(ctx) {
|
|
|
1130
1428
|
risk_downgrades: state.risk_downgrades ?? [],
|
|
1131
1429
|
}, null, 2);
|
|
1132
1430
|
}
|
|
1431
|
+
if (a.operation === 'sonar_check') {
|
|
1432
|
+
if (!state.sonar_policy)
|
|
1433
|
+
throw new Error('SonarQube is not enabled for this task');
|
|
1434
|
+
if (state.stage !== '代码审核')
|
|
1435
|
+
throw new Error('sonar_check runs only in the 代码审核 stage');
|
|
1436
|
+
if (!state.verification.passed || (await evidenceBlockers(fs, state, workflow, cwd)).length) {
|
|
1437
|
+
throw new Error('functional verification must pass on the current files before sonar_check');
|
|
1438
|
+
}
|
|
1439
|
+
if (!state.commits.some(commit => commit.hash !== undefined)) {
|
|
1440
|
+
throw new Error('sonar_check requires a recorded Git commit; commit at the 测试 checkpoint, push it, and use that CI scan');
|
|
1441
|
+
}
|
|
1442
|
+
if (!a.ce_task_id)
|
|
1443
|
+
throw new Error('sonar_check requires ce_task_id from this task\'s completed CI scan');
|
|
1444
|
+
const target = state.sonar_policy.mode === 'branch' ? state.branch : a.pull_request;
|
|
1445
|
+
if (!target)
|
|
1446
|
+
throw new Error('pull-request SonarQube audit requires pull_request');
|
|
1447
|
+
const audit = await inspectSonar(state.sonar_policy, process.env[state.sonar_policy.token_env] ?? '', a.ce_task_id, target, await scopeFingerprint(fs, state, cwd));
|
|
1448
|
+
audit.commit_hash = state.commits.findLast(entry => entry.hash !== undefined).hash;
|
|
1449
|
+
if (state.sonar_audit !== undefined)
|
|
1450
|
+
state.sonar_history = [...(state.sonar_history ?? []), state.sonar_audit].slice(-20);
|
|
1451
|
+
state.sonar_audit = audit;
|
|
1452
|
+
state.review.outcome = 'pending';
|
|
1453
|
+
await writeTask(fs, state, cwd, await resolveWriteMode(ctx, a, exec));
|
|
1454
|
+
return JSON.stringify({ gate: audit.gate, blocking: audit.blocking, findings: audit.findings,
|
|
1455
|
+
analysis_id: audit.analysis_id, next_action: audit.gate === 'OK' && audit.blocking.length === 0
|
|
1456
|
+
? 'Continue code review, then record review pass.'
|
|
1457
|
+
: 'Record review blocked, fix findings, rerun tests and CI scan, then call sonar_check with the new ce_task_id.' }, null, 2);
|
|
1458
|
+
}
|
|
1459
|
+
if (a.operation === 'learn_rule') {
|
|
1460
|
+
const issue = [state.sonar_audit, ...(state.sonar_history ?? [])].filter(entry => entry !== undefined)
|
|
1461
|
+
.flatMap(entry => entry.blocking).find(entry => entry.key === a.issue_key);
|
|
1462
|
+
if (!issue)
|
|
1463
|
+
throw new Error('learn_rule requires a blocking issue_key recorded by this task\'s SonarQube audit');
|
|
1464
|
+
if (!a.rule_name || !RESOURCE_NAME.test(a.rule_name) || !a.content?.trim() || a.content.trim().length < 40
|
|
1465
|
+
|| !a.learning_reason?.trim()) {
|
|
1466
|
+
throw new Error('learn_rule needs a new kebab-case rule_name, actionable content of at least 40 characters, and learning_reason');
|
|
1467
|
+
}
|
|
1468
|
+
const codeSkills = workflow.stage_bindings?.['代码开发']?.skills ?? [];
|
|
1469
|
+
const binding = codeSkills.find(entry => formatResourceRef(entry.skill) === a.skill_name || entry.skill.name === a.skill_name);
|
|
1470
|
+
if (!binding)
|
|
1471
|
+
throw new Error('learn_rule requires a Skill bound to the 代码开发 meta-skill');
|
|
1472
|
+
const rulePath = `.dsh/rules/${a.rule_name}.md`;
|
|
1473
|
+
const skillPath = `.dsh/skills/${binding.skill.name}/SKILL.md`;
|
|
1474
|
+
const profilePath = `.dsh/skills/${binding.skill.name}/profile.json`;
|
|
1475
|
+
const root = state.root ?? cwd ?? '';
|
|
1476
|
+
if (!cwd || resolve(root) !== resolve(cwd))
|
|
1477
|
+
throw new Error('learn_rule must run with the task project root as the session workspace');
|
|
1478
|
+
for (const path of [rulePath, skillPath, profilePath])
|
|
1479
|
+
assertInsideRoot(root, cwd ?? root, path);
|
|
1480
|
+
if (await readText(fs, rulePath, cwd) !== undefined)
|
|
1481
|
+
throw new Error(`project Rule ${a.rule_name} already exists; edit it through the Rule editor instead`);
|
|
1482
|
+
const projectSkill = await readText(fs, skillPath, cwd);
|
|
1483
|
+
const sourceSkill = projectSkill ?? await readSkillAt(binding.skill, fs, cwd);
|
|
1484
|
+
if (sourceSkill === undefined)
|
|
1485
|
+
throw new Error(`Skill ${formatResourceRef(binding.skill)} cannot be read`);
|
|
1486
|
+
const profileRaw = await readText(fs, profilePath, cwd);
|
|
1487
|
+
const baseProfile = projectSkill === undefined ? { rules: binding.rules, evidence: binding.evidence }
|
|
1488
|
+
: await profileFor({ source: 'project', name: binding.skill.name }, fs, cwd) ?? { rules: [], evidence: binding.evidence };
|
|
1489
|
+
const profile = { rules: [...baseProfile.rules, { source: 'project', name: a.rule_name }],
|
|
1490
|
+
...(baseProfile.evidence !== undefined ? { evidence: baseProfile.evidence } : {}) };
|
|
1491
|
+
const ruleBody = `# ${a.rule_name}\n\n${a.content.trimEnd()}\n\n` +
|
|
1492
|
+
`> 来源:SonarQube 规则 ${issue.rule}、问题 ${issue.key}。可复用原因:${a.learning_reason.trim()}\n`;
|
|
1493
|
+
const proposalHash = hashText(JSON.stringify({ issue: issue.key, skill: binding.skill, name: a.rule_name,
|
|
1494
|
+
body: ruleBody, profile, profile_hash: hashText(profileRaw ?? '') }));
|
|
1495
|
+
const phase = a.phase ?? 'propose';
|
|
1496
|
+
if (phase === 'propose')
|
|
1497
|
+
return JSON.stringify({ issue, target_skill: binding.skill,
|
|
1498
|
+
project_override: projectSkill === undefined, rule_path: rulePath, rule_body: ruleBody,
|
|
1499
|
+
profile_path: profilePath, profile, expected_hash: proposalHash,
|
|
1500
|
+
instruction: 'Review the example and generality before apply. The project Rule will affect future tasks; current task bindings remain frozen.' }, null, 2);
|
|
1501
|
+
if (phase !== 'apply' || a.expected_hash !== proposalHash)
|
|
1502
|
+
throw new Error('learn_rule apply requires the unchanged expected_hash from propose');
|
|
1503
|
+
const mode = await resolveWriteMode(ctx, a, exec);
|
|
1504
|
+
await writeText(fs, rulePath, ruleBody, cwd, mode, { kind: 'createIfAbsent' });
|
|
1505
|
+
if (projectSkill === undefined) {
|
|
1506
|
+
await writeText(fs, skillPath, sourceSkill, cwd, mode, { kind: 'createIfAbsent' });
|
|
1507
|
+
}
|
|
1508
|
+
const info = await fs.lstat(profilePath, { cwd });
|
|
1509
|
+
await writeText(fs, profilePath, JSON.stringify(profile, null, 2) + '\n', cwd, mode, info === undefined ? { kind: 'createIfAbsent' } : { kind: 'replaceIfVersion', version: info.version });
|
|
1510
|
+
state.learned_rules = [...(state.learned_rules ?? []), { issue_key: issue.key, rule_name: a.rule_name,
|
|
1511
|
+
skill_name: binding.skill.name, at: new Date().toISOString() }];
|
|
1512
|
+
await writeTask(fs, state, cwd, mode);
|
|
1513
|
+
return JSON.stringify({ created_rule: rulePath, attached_to: `project:${binding.skill.name}`,
|
|
1514
|
+
next_action: 'Review this file in Git. New adaptive tasks resolve the project Skill and its Rule automatically.' }, null, 2);
|
|
1515
|
+
}
|
|
1133
1516
|
if (a.operation === 'commit') {
|
|
1134
1517
|
await assertFreshEvidence(fs, state, workflow, cwd);
|
|
1135
1518
|
const missingSkills = skillBlockers(state, workflow, session);
|
|
@@ -1232,6 +1615,10 @@ export function registerDevTask(ctx) {
|
|
|
1232
1615
|
kind, reason: a.revision_reason.trim(), to: target, from: state.stage,
|
|
1233
1616
|
at: new Date().toISOString(), invalidated: invalidatedBy(kind),
|
|
1234
1617
|
});
|
|
1618
|
+
if (state.sonar_audit !== undefined) {
|
|
1619
|
+
state.sonar_history = [...(state.sonar_history ?? []), state.sonar_audit].slice(-20);
|
|
1620
|
+
}
|
|
1621
|
+
delete state.sonar_audit;
|
|
1235
1622
|
await writeTask(fs, state, cwd, await resolveWriteMode(ctx, a, exec));
|
|
1236
1623
|
return 'revised (' + kind + ') back to "' + outcome.stage + '"; invalidated: ' + outcome.invalidated.join(', ')
|
|
1237
1624
|
+ '. Re-establish these before advancing again; the history is kept in the task record.';
|
|
@@ -1406,7 +1793,8 @@ export function registerDevTask(ctx) {
|
|
|
1406
1793
|
}
|
|
1407
1794
|
case 'skill_result': {
|
|
1408
1795
|
const stage = a.target_stage ?? state.stage;
|
|
1409
|
-
const entry = (workflow.stage_bindings?.[stage]?.skills ?? []).find(binding => (binding.skill.source === 'codex-project'
|
|
1796
|
+
const entry = (workflow.stage_bindings?.[stage]?.skills ?? []).find(binding => (state.complexity !== undefined || binding.skill.source === 'codex-project'
|
|
1797
|
+
? formatResourceRef(binding.skill) : binding.skill.name) === a.skill_name);
|
|
1410
1798
|
if (!obligationStages(state, workflow).includes(stage) || !entry)
|
|
1411
1799
|
throw new Error('skill_result requires a skill bound to this stage or its upcoming terminal stage');
|
|
1412
1800
|
if ((await readSkillAt(entry.skill, fs, cwd)) === undefined) {
|
|
@@ -1416,10 +1804,12 @@ export function registerDevTask(ctx) {
|
|
|
1416
1804
|
if (unresolvedForSkill.length > 0) {
|
|
1417
1805
|
throw new Error(`bound rule for ${formatResourceRef(entry.skill)} resolves nowhere: ${unresolvedForSkill.join(', ')}`);
|
|
1418
1806
|
}
|
|
1419
|
-
const
|
|
1420
|
-
const
|
|
1807
|
+
const exactLoadKey = `${state.id}#${formatResourceRef(entry.skill)}`;
|
|
1808
|
+
const loaded = loadedSkills(session);
|
|
1809
|
+
const loadCall = loaded.get(exactLoadKey) ?? (state.complexity === undefined && entry.skill.source !== 'codex-project'
|
|
1810
|
+
? loaded.get(entry.skill.name) : undefined);
|
|
1421
1811
|
if (!loadCall)
|
|
1422
|
-
throw new Error(entry.skill.source === 'codex-project'
|
|
1812
|
+
throw new Error(state.complexity !== undefined || entry.skill.source === 'codex-project'
|
|
1423
1813
|
? `load skill "${a.skill_name}" with dev_task operation=load_skill before recording execution`
|
|
1424
1814
|
: `load skill "${a.skill_name}" successfully before recording execution`);
|
|
1425
1815
|
if (entry.evidence === 'manual') {
|
|
@@ -1455,6 +1845,11 @@ export function registerDevTask(ctx) {
|
|
|
1455
1845
|
case 'review':
|
|
1456
1846
|
if (a.outcome !== 'pass' && a.outcome !== 'blocked')
|
|
1457
1847
|
throw new Error('review requires outcome: pass|blocked');
|
|
1848
|
+
if (a.outcome === 'pass') {
|
|
1849
|
+
const sonar = await sonarBlockers(fs, state, cwd);
|
|
1850
|
+
if (sonar.length)
|
|
1851
|
+
throw new Error(sonar.join('; '));
|
|
1852
|
+
}
|
|
1458
1853
|
state.review.outcome = a.outcome;
|
|
1459
1854
|
break;
|
|
1460
1855
|
default:
|