iterate-plugin 2.5.0 → 2.7.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.
@@ -0,0 +1,134 @@
1
+ import { join } from 'node:path';
2
+ import { defineTool } from '@deepseek-ai/dsh-tools';
3
+ import { loadEffectiveConfig, validateConfig, resolveProjectRoot } from "../config-loader.js";
4
+ import { applyConfigUpdates, readRawConfig, validateConfigUpdates, writeConfigFile, } from "../config-write.js";
5
+ /**
6
+ * Register the `iterate_config` tool.
7
+ * Reads and returns the iterate.config.yaml configuration, or writes a
8
+ * validated partial update back to it (with backup + rollback).
9
+ * Model-facing: returns JSON with the full config, a specific section, or validation errors.
10
+ */
11
+ export function registerConfigTool(ctx) {
12
+ ctx.tools.register(defineTool({
13
+ name: 'iterate_config',
14
+ description: 'Read or update the iterate.config.yaml configuration from the project root. ' +
15
+ 'Returns the full parsed config, a specific section, or validation errors. ' +
16
+ 'Use this to discover available dimensions, validation commands, git settings, and personalization rules, ' +
17
+ 'or to write back validated changes (goal, dimensions, max_rounds, review, atomic, validation, git, etc.).',
18
+ parameters: {
19
+ operation: {
20
+ type: 'string',
21
+ description: 'Default "read". "write" validates and applies a partial config update (backed up first).',
22
+ enum: ['read', 'write'],
23
+ },
24
+ path: {
25
+ type: 'string',
26
+ description: 'Project root directory (default: current working directory).',
27
+ },
28
+ section: {
29
+ type: 'string',
30
+ description: 'Optional config section to return: dimensions, validation, git, atomic, review, personalization, onboarding, or goal.',
31
+ },
32
+ validate: {
33
+ type: 'boolean',
34
+ description: 'If true, validate the config schema and return any missing fields.',
35
+ },
36
+ updates: {
37
+ type: 'json',
38
+ description: 'For operation "write": a partial config object to merge in, e.g. ' +
39
+ '{"goal":"...","dimensions":["correctness","security"],"max_rounds":5}. ' +
40
+ 'Supported keys: goal, language, dimensions, max_rounds, review, atomic, git, validation, personalization, onboarding.',
41
+ },
42
+ },
43
+ output: {
44
+ schema: {
45
+ type: 'object',
46
+ additionalProperties: false,
47
+ properties: {
48
+ found: { type: 'boolean', required: true },
49
+ valid: { type: 'boolean' },
50
+ errors: { type: 'array', items: { type: 'string' } },
51
+ section: { type: 'string' },
52
+ data: { type: 'json' },
53
+ config: { type: 'json' },
54
+ availableSections: { type: 'array', items: { type: 'string' } },
55
+ operation: { type: 'string' },
56
+ ok: { type: 'boolean' },
57
+ backupPath: { type: 'string' },
58
+ error: { type: 'string' },
59
+ },
60
+ },
61
+ render: (_args, value) => [
62
+ { type: 'text', text: JSON.stringify(value, null, 2) },
63
+ ],
64
+ },
65
+ async execute(args) {
66
+ const resolved = resolveProjectRoot(args.path);
67
+ if (!resolved.ok) {
68
+ return { found: false, error: resolved.reason };
69
+ }
70
+ const projectRoot = resolved.root;
71
+ // ── Write operation ────────────────────────────────────────────────
72
+ if (args.operation === 'write') {
73
+ const updates = args.updates;
74
+ const updateErrors = validateConfigUpdates(updates ?? {});
75
+ if (updateErrors.length > 0) {
76
+ return { operation: 'write', ok: false, found: false, errors: updateErrors };
77
+ }
78
+ let base;
79
+ try {
80
+ base = readRawConfig(join(projectRoot, 'iterate.config.yaml'));
81
+ }
82
+ catch (err) {
83
+ return { operation: 'write', ok: false, found: false, error: `failed to read config: ${String(err)}` };
84
+ }
85
+ const merged = applyConfigUpdates(base, updates ?? {});
86
+ const schemaErrors = validateConfig(merged);
87
+ if (schemaErrors.length > 0) {
88
+ return {
89
+ operation: 'write',
90
+ ok: false,
91
+ found: false,
92
+ errors: schemaErrors.map((e) => `missing/required field: ${e}`),
93
+ };
94
+ }
95
+ const result = writeConfigFile(projectRoot, merged);
96
+ if (!result.ok)
97
+ return { operation: 'write', ok: false, found: false, error: result.error };
98
+ const { config } = loadEffectiveConfig(projectRoot);
99
+ return {
100
+ operation: 'write',
101
+ ok: true,
102
+ found: true,
103
+ backupPath: result.backupPath ?? undefined,
104
+ config: config,
105
+ };
106
+ }
107
+ // ── Read operations (original behavior) ────────────────────────────
108
+ const { config, source } = loadEffectiveConfig(projectRoot);
109
+ const hasOverride = source === 'override';
110
+ if (args.validate) {
111
+ const errors = validateConfig(config);
112
+ return {
113
+ found: hasOverride,
114
+ valid: errors.length === 0,
115
+ errors: errors.length > 0 ? errors : undefined,
116
+ section: 'validation_report',
117
+ };
118
+ }
119
+ if (args.section) {
120
+ const configRecord = config;
121
+ const section = configRecord[args.section];
122
+ if (section === undefined) {
123
+ return {
124
+ found: hasOverride,
125
+ error: `Section "${args.section}" not found in config.`,
126
+ availableSections: Object.keys(configRecord),
127
+ };
128
+ }
129
+ return { found: hasOverride, section: args.section, data: section };
130
+ }
131
+ return { found: hasOverride, config: config };
132
+ },
133
+ }));
134
+ }
@@ -0,0 +1,160 @@
1
+ import { readFileSync, existsSync } from 'node:fs';
2
+ import { join, dirname, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { defineTool } from '@deepseek-ai/dsh-tools';
5
+ import { resolveProjectRoot } from "../config-loader.js";
6
+ /** How many ancestor directories we walk up looking for a SKILL.md. */
7
+ const MAX_SKILL_DIR_LOOKUP_DEPTH = 12;
8
+ /**
9
+ * The directory this source file lives in (…/src/tools). The plugin's own
10
+ * package root is one level up (…/src), and the skill root is typically a few
11
+ * levels above that. We use it as the anchor for auto-detecting where the
12
+ * original SKILL.md lives.
13
+ */
14
+ const PLUGIN_SRC_DIR = dirname(fileURLToPath(import.meta.url));
15
+ /**
16
+ * Walk up from the plugin's own location until a directory containing SKILL.md
17
+ * is found. This is how the plugin locates the ORIGINAL skill (skill 目录)
18
+ * without any hardcoded absolute path — it works whether the plugin is mounted
19
+ * from the source tree or bundled next to the skill.
20
+ *
21
+ * Returns the absolute directory containing SKILL.md, or null if none found
22
+ * within `MAX_SKILL_DIR_LOOKUP_DEPTH` ancestors.
23
+ */
24
+ function findSkillRoot(startDir) {
25
+ let dir = resolve(startDir);
26
+ for (let depth = 0; depth < MAX_SKILL_DIR_LOOKUP_DEPTH; depth++) {
27
+ if (existsSync(join(dir, 'SKILL.md')))
28
+ return dir;
29
+ const parent = dirname(dir);
30
+ if (parent === dir)
31
+ break; // reached the filesystem root
32
+ dir = parent;
33
+ }
34
+ return null;
35
+ }
36
+ /** Exported for unit tests. See the private `findSkillRoot` above. */
37
+ export { findSkillRoot };
38
+ /**
39
+ * Read a file from a candidate directory, returning its content or null.
40
+ */
41
+ function readProjectFile(projectRoot, filename) {
42
+ const filePath = join(projectRoot, filename);
43
+ if (!existsSync(filePath))
44
+ return null;
45
+ try {
46
+ return readFileSync(filePath, 'utf-8');
47
+ }
48
+ catch {
49
+ return null;
50
+ }
51
+ }
52
+ /**
53
+ * Locate the first existing SKILL.md across the candidate directories, in
54
+ * priority order:
55
+ * 1. explicit skillDir (custom path / 自定义路径)
56
+ * 2. auto-detected skill root walking up from the plugin (skill 目录)
57
+ * 3. project root (项目根)
58
+ * Returns the file content plus the directory it was found in, or null.
59
+ */
60
+ function findSkillMd(candidates) {
61
+ for (const dir of candidates) {
62
+ if (!dir)
63
+ continue;
64
+ const content = readProjectFile(dir, 'SKILL.md');
65
+ if (content !== null)
66
+ return { content, sourceDir: dir };
67
+ }
68
+ return null;
69
+ }
70
+ /** Exported for unit tests. See the private `findSkillMd` above. */
71
+ export { findSkillMd };
72
+ /**
73
+ * Register the `iterate_context` tool.
74
+ * Reads SKILL.md (original skill instructions) from the skill directory,
75
+ * project root, or a custom path, and ITERATE.md from the project root.
76
+ * Provides the model with the original skill instructions and project knowledge base.
77
+ */
78
+ export function registerContextTool(ctx) {
79
+ ctx.tools.register(defineTool({
80
+ name: 'iterate_context',
81
+ description: 'Read project context files (SKILL.md and/or ITERATE.md). ' +
82
+ 'SKILL.md contains the original iterate skill instructions; it is searched in ' +
83
+ 'the skill directory (auto-detected), the project root, or an explicit `skillDir`. ' +
84
+ 'ITERATE.md contains the project-specific knowledge base and onboarding information. ' +
85
+ 'Use this to understand the skill workflow and project context.',
86
+ parameters: {
87
+ files: {
88
+ type: 'string',
89
+ required: true,
90
+ description: 'Comma-separated list of files to read: "skill", "project", or "skill,project" for both.',
91
+ },
92
+ path: {
93
+ type: 'string',
94
+ description: 'Project root directory (default: current working directory).',
95
+ },
96
+ skillDir: {
97
+ type: 'string',
98
+ description: 'Custom directory to search for SKILL.md (highest priority). ' +
99
+ 'When omitted, SKILL.md is auto-detected from the skill directory, then the project root.',
100
+ },
101
+ },
102
+ output: {
103
+ schema: {
104
+ type: 'object',
105
+ additionalProperties: false,
106
+ properties: {
107
+ found: { type: 'boolean', required: true },
108
+ skill: { oneOf: [{ type: 'string' }, { type: 'null' }] },
109
+ project: { oneOf: [{ type: 'string' }, { type: 'null' }] },
110
+ skillSource: { oneOf: [{ type: 'string' }, { type: 'null' }] },
111
+ error: { type: 'string' },
112
+ searched: { type: 'array', items: { type: 'string' } },
113
+ },
114
+ },
115
+ render: (_args, value) => {
116
+ const parts = [];
117
+ if (value.skill)
118
+ parts.push(`--- SKILL.md (${value.skillSource ?? '?source?'}) ---\n${value.skill}`);
119
+ if (value.project)
120
+ parts.push(`--- ITERATE.md ---\n${value.project}`);
121
+ if (!value.skill && !value.project) {
122
+ parts.push('No files found. Searched: ' + (value.searched?.join(', ') ?? 'none'));
123
+ }
124
+ return [{ type: 'text', text: parts.join('\n\n') }];
125
+ },
126
+ },
127
+ async execute(args) {
128
+ const resolved = resolveProjectRoot(args.path);
129
+ if (!resolved.ok) {
130
+ return { found: false, error: resolved.reason, searched: [] };
131
+ }
132
+ const projectRoot = resolved.root;
133
+ const requested = (args.files ?? '')
134
+ .split(',')
135
+ .map((s) => s.trim().toLowerCase())
136
+ .filter(Boolean);
137
+ const result = { found: true, searched: [] };
138
+ if (requested.includes('skill') || requested.includes('skill.md')) {
139
+ // Candidate dirs in priority order: custom path → auto-detected skill
140
+ // root → project root. This is how "skill 目录、项目根、自定义路径"
141
+ // are all supported.
142
+ const skillRoot = findSkillRoot(PLUGIN_SRC_DIR);
143
+ const candidates = [];
144
+ if (args.skillDir)
145
+ candidates.push(args.skillDir);
146
+ if (skillRoot)
147
+ candidates.push(skillRoot);
148
+ candidates.push(projectRoot);
149
+ result.searched = candidates;
150
+ const found = findSkillMd(candidates);
151
+ result.skill = found ? found.content : null;
152
+ result.skillSource = found ? found.sourceDir : null;
153
+ }
154
+ if (requested.includes('project') || requested.includes('iterate.md')) {
155
+ result.project = readProjectFile(projectRoot, 'ITERATE.md');
156
+ }
157
+ return result;
158
+ },
159
+ }));
160
+ }
@@ -0,0 +1,162 @@
1
+ import { appendFileSync, readFileSync, mkdirSync, existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { defineTool } from '@deepseek-ai/dsh-tools';
4
+ import { resolveProjectRoot } from "../config-loader.js";
5
+ const LOG_DIR = '.iterate';
6
+ const LOG_FILE = 'decision-log.jsonl';
7
+ /**
8
+ * Resolve the log file path, creating the directory if needed.
9
+ */
10
+ function logPath(projectRoot) {
11
+ const dir = join(projectRoot, LOG_DIR);
12
+ if (!existsSync(dir)) {
13
+ mkdirSync(dir, { recursive: true });
14
+ }
15
+ return join(dir, LOG_FILE);
16
+ }
17
+ /**
18
+ * Append one entry to the decision log (JSONL format).
19
+ * Returns the entry count after appending.
20
+ */
21
+ export function appendDecisionEntry(projectRoot, entry) {
22
+ const filePath = logPath(projectRoot);
23
+ const line = JSON.stringify(entry) + '\n';
24
+ appendFileSync(filePath, line, 'utf-8');
25
+ // Count entries
26
+ let count = 0;
27
+ try {
28
+ const content = readFileSync(filePath, 'utf-8');
29
+ count = content.split('\n').filter((l) => l.trim().length > 0).length;
30
+ }
31
+ catch {
32
+ count = 1;
33
+ }
34
+ return { count, path: filePath };
35
+ }
36
+ /**
37
+ * Read all entries from the decision log.
38
+ */
39
+ export function readDecisionEntries(projectRoot) {
40
+ const filePath = join(projectRoot, LOG_DIR, LOG_FILE);
41
+ if (!existsSync(filePath))
42
+ return [];
43
+ try {
44
+ const content = readFileSync(filePath, 'utf-8');
45
+ return content
46
+ .split('\n')
47
+ .filter((l) => l.trim().length > 0)
48
+ .map((l) => JSON.parse(l));
49
+ }
50
+ catch {
51
+ return [];
52
+ }
53
+ }
54
+ /**
55
+ * Register the `iterate_decision_log` tool.
56
+ * Append-only decision log stored in .iterate/decision-log.jsonl.
57
+ * Supports `append` and `read` operations.
58
+ */
59
+ export function registerDecisionLogTool(ctx) {
60
+ ctx.tools.register(defineTool({
61
+ name: 'iterate_decision_log',
62
+ description: 'Append-only decision log for the iterate loop. ' +
63
+ 'Use `append` to record a round start, review finding, fix, validation result, or decision. ' +
64
+ 'Use `read` to retrieve all entries for review. ' +
65
+ 'The log is stored in .iterate/decision-log.jsonl and persists across sessions.',
66
+ parameters: {
67
+ operation: {
68
+ type: 'string',
69
+ required: true,
70
+ description: '"append" to add an entry, "read" to retrieve all entries.',
71
+ enum: ['append', 'read'],
72
+ },
73
+ type: {
74
+ type: 'string',
75
+ description: 'Entry type (required for append): round_start, review_result, atomic_fix, ' +
76
+ 'architectural_fix, revert, validation, decision, report.',
77
+ enum: [
78
+ 'round_start',
79
+ 'review_result',
80
+ 'atomic_fix',
81
+ 'architectural_fix',
82
+ 'revert',
83
+ 'validation',
84
+ 'decision',
85
+ 'report',
86
+ ],
87
+ },
88
+ round: {
89
+ type: 'integer',
90
+ description: 'Current iteration round number (required for append).',
91
+ },
92
+ data: {
93
+ type: 'json',
94
+ description: 'Entry payload as JSON object (required for append).',
95
+ },
96
+ path: {
97
+ type: 'string',
98
+ description: 'Project root directory (default: current working directory).',
99
+ },
100
+ },
101
+ output: {
102
+ schema: {
103
+ type: 'object',
104
+ additionalProperties: false,
105
+ properties: {
106
+ operation: { type: 'string', required: true },
107
+ entryCount: { type: 'integer' },
108
+ logPath: { type: 'string' },
109
+ entries: { type: 'json' },
110
+ success: { type: 'boolean' },
111
+ entry: { type: 'json' },
112
+ error: { type: 'string' },
113
+ },
114
+ },
115
+ render: (_args, value) => [
116
+ { type: 'text', text: JSON.stringify(value, null, 2) },
117
+ ],
118
+ },
119
+ async execute(args) {
120
+ const resolved = resolveProjectRoot(args.path);
121
+ if (!resolved.ok) {
122
+ return { operation: args.operation, error: resolved.reason };
123
+ }
124
+ const projectRoot = resolved.root;
125
+ if (args.operation === 'read') {
126
+ const entries = readDecisionEntries(projectRoot);
127
+ return {
128
+ operation: 'read',
129
+ entryCount: entries.length,
130
+ logPath: join(projectRoot, LOG_DIR, LOG_FILE),
131
+ entries: entries,
132
+ };
133
+ }
134
+ if (args.operation === 'append') {
135
+ if (!args.type || !args.round) {
136
+ return {
137
+ operation: 'append',
138
+ error: 'type and round are required for append operation.',
139
+ };
140
+ }
141
+ const entry = {
142
+ timestamp: new Date().toISOString(),
143
+ round: args.round,
144
+ type: args.type,
145
+ data: args.data ?? {},
146
+ };
147
+ const result = appendDecisionEntry(projectRoot, entry);
148
+ return {
149
+ operation: 'append',
150
+ success: true,
151
+ entryCount: result.count,
152
+ logPath: result.path,
153
+ entry: entry,
154
+ };
155
+ }
156
+ return {
157
+ operation: args.operation,
158
+ error: `Unknown operation "${args.operation}". Use "append" or "read".`,
159
+ };
160
+ },
161
+ }));
162
+ }