@240xu/dsh-tech-lead-plugin 0.3.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/package.json +42 -0
- package/src/index.js +33 -0
- package/src/protocol.js +119 -0
- package/src/tools/context.js +94 -0
- package/src/tools/gates.js +93 -0
- package/src/tools/mutation.js +18 -0
- package/src/tools/progress.js +77 -0
- package/src/tools.js +221 -0
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@240xu/dsh-tech-lead-plugin",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Read-only tech-lead lifecycle tools for DeepSeek Harness (classify, validate, lint, audit)",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "src/index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./src/index.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"src/"
|
|
13
|
+
],
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/240xu/tech-lead-skill.git",
|
|
17
|
+
"directory": "packages/dsh-tech-lead-plugin"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@240xu/dsh-tech-lead-core": "workspace:^",
|
|
21
|
+
"@deepseek-ai/dsh-tools": "0.1.0-rc.7"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@deepseek-ai/cordis": "4.0.1",
|
|
25
|
+
"@deepseek-ai/cordis-plugin-include": "^1.0.6",
|
|
26
|
+
"@deepseek-ai/cordis-plugin-loader": "^1.0.2"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=16"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"keywords": [
|
|
35
|
+
"dsh",
|
|
36
|
+
"deepseek-harness",
|
|
37
|
+
"cordis",
|
|
38
|
+
"tech-lead",
|
|
39
|
+
"plugin",
|
|
40
|
+
"opencode"
|
|
41
|
+
]
|
|
42
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
2
|
+
import {
|
|
3
|
+
classify, validateState, transitionCheck,
|
|
4
|
+
evidenceLint, planLint, gatePrecheck,
|
|
5
|
+
releaseAudit, installAudit, resumeCard,
|
|
6
|
+
getCapabilities,
|
|
7
|
+
validateContext, evidenceGraphLint, evidenceFreshness,
|
|
8
|
+
progressDecide, criticalPath, changeImpact,
|
|
9
|
+
gatePlan, gateAggregate, gateReopen, previewMutation,
|
|
10
|
+
} from '@240xu/dsh-tech-lead-core';
|
|
11
|
+
import { registerTools } from './tools.js';
|
|
12
|
+
|
|
13
|
+
export const name = 'tech-lead-tools';
|
|
14
|
+
export const inject = ['tools'];
|
|
15
|
+
export { getCapabilities };
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Registers the read-only tech-lead tool surface. Every tool computes over
|
|
19
|
+
* caller-supplied JSON — no filesystem writes, no subprocesses, no network.
|
|
20
|
+
* @param {import('@deepseek-ai/cordis').Context} ctx
|
|
21
|
+
*/
|
|
22
|
+
export function apply(ctx) {
|
|
23
|
+
for (const tool of registerTools(defineTool, {
|
|
24
|
+
classify, validateState, transitionCheck,
|
|
25
|
+
evidenceLint, planLint, gatePrecheck,
|
|
26
|
+
releaseAudit, installAudit, resumeCard,
|
|
27
|
+
validateContext, evidenceGraphLint, evidenceFreshness,
|
|
28
|
+
progressDecide, criticalPath, changeImpact,
|
|
29
|
+
gatePlan, gateAggregate, gateReopen, previewMutation,
|
|
30
|
+
})) {
|
|
31
|
+
ctx.tools.register(tool);
|
|
32
|
+
}
|
|
33
|
+
}
|
package/src/protocol.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { errorEnvelope } from '@240xu/dsh-tech-lead-core';
|
|
2
|
+
|
|
3
|
+
export function parseJsonString(value, path = 'input') {
|
|
4
|
+
if (typeof value !== 'string') {
|
|
5
|
+
return { ok: false, error: { code: 'BAD_INPUT', path, message: 'expected JSON text string' } };
|
|
6
|
+
}
|
|
7
|
+
try {
|
|
8
|
+
return { ok: true, value: JSON.parse(value) };
|
|
9
|
+
} catch (error) {
|
|
10
|
+
return { ok: false, error: { code: 'BAD_INPUT', path, message: `invalid JSON: ${error.message}` } };
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function parseJsonFields(args, fields) {
|
|
15
|
+
const values = {};
|
|
16
|
+
const errors = [];
|
|
17
|
+
for (const field of fields) {
|
|
18
|
+
const result = parseJsonString(args?.[field], field);
|
|
19
|
+
if (result.ok) values[field] = result.value;
|
|
20
|
+
else errors.push(result.error);
|
|
21
|
+
}
|
|
22
|
+
return errors.length ? { ok: false, errors } : { ok: true, values };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const FINDINGS_LIMIT = 500;
|
|
26
|
+
const ECHO_LIMIT = 100;
|
|
27
|
+
const RESULT_ARRAY_LIMIT = 1000;
|
|
28
|
+
const WALK_DEPTH_LIMIT = 64;
|
|
29
|
+
const COMPACT_THRESHOLD = 262144;
|
|
30
|
+
const ECHO_KEYS = new Set(['evidence', 'targets', 'expectedDiff', 'verification', 'items']);
|
|
31
|
+
|
|
32
|
+
export function clampEnvelope(envelope) {
|
|
33
|
+
if (Array.isArray(envelope)) return envelope.length > FINDINGS_LIMIT ? envelope.slice(0, FINDINGS_LIMIT) : envelope;
|
|
34
|
+
if (envelope === null || typeof envelope !== 'object') return envelope;
|
|
35
|
+
const out = { ...envelope };
|
|
36
|
+
let truncatedTotal = 0;
|
|
37
|
+
for (const field of ['errors', 'warnings']) {
|
|
38
|
+
if (Array.isArray(out[field]) && out[field].length > FINDINGS_LIMIT) {
|
|
39
|
+
truncatedTotal = Math.max(truncatedTotal, out[field].length);
|
|
40
|
+
out[field] = out[field].slice(0, FINDINGS_LIMIT);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (truncatedTotal > 0) {
|
|
44
|
+
out.warnings = [...(out.warnings ?? []), { code: 'FINDINGS_TRUNCATED', total: truncatedTotal, message: `output truncated to first ${FINDINGS_LIMIT} entries per findings field` }];
|
|
45
|
+
}
|
|
46
|
+
if (out.data !== null && typeof out.data === 'object') {
|
|
47
|
+
out.data = clampNode(out.data, 0);
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Iterative walk with a hard depth cap: subtrees deeper than WALK_DEPTH_LIMIT are
|
|
53
|
+
// passed through untouched (native JSON serialization handles arbitrary depth),
|
|
54
|
+
// so hostile nesting can no longer turn the renderer into an INTERNAL error.
|
|
55
|
+
function clampNode(root, rootDepth) {
|
|
56
|
+
const result = Array.isArray(root) ? [...root] : { ...root };
|
|
57
|
+
const stack = [[result, rootDepth]];
|
|
58
|
+
while (stack.length) {
|
|
59
|
+
const [node, depth] = stack.pop();
|
|
60
|
+
const entries = Object.keys(node);
|
|
61
|
+
for (const key of entries) {
|
|
62
|
+
const value = node[key];
|
|
63
|
+
if (!value || typeof value !== 'object') continue;
|
|
64
|
+
if (depth + 1 > WALK_DEPTH_LIMIT) {
|
|
65
|
+
node[key] = { truncated: true, reason: 'DEPTH_LIMIT', depth: WALK_DEPTH_LIMIT };
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (Array.isArray(value)) {
|
|
69
|
+
if (ECHO_KEYS.has(key) && value.length > ECHO_LIMIT) {
|
|
70
|
+
node[key] = { truncated: true, total: value.length };
|
|
71
|
+
} else if (value.length > RESULT_ARRAY_LIMIT) {
|
|
72
|
+
node[key] = value.slice(0, RESULT_ARRAY_LIMIT);
|
|
73
|
+
} else {
|
|
74
|
+
node[key] = [...value];
|
|
75
|
+
stack.push([node[key], depth + 1]);
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
node[key] = { ...value };
|
|
79
|
+
stack.push([node[key], depth + 1]);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function canonicalStringify(value) {
|
|
87
|
+
return canonicalize(value, 0);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function canonicalize(value, depth) {
|
|
91
|
+
if (depth > WALK_DEPTH_LIMIT) return null; // deep subtrees compare as equal; drift beyond the cap is unreported by design
|
|
92
|
+
if (Array.isArray(value)) return value.map((item) => canonicalize(item, depth + 1));
|
|
93
|
+
if (value !== null && typeof value === 'object') {
|
|
94
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonicalize(value[key], depth + 1)]));
|
|
95
|
+
}
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function runGuarded(operation, fn) {
|
|
100
|
+
try {
|
|
101
|
+
return fn();
|
|
102
|
+
} catch (error) {
|
|
103
|
+
return JSON.stringify(
|
|
104
|
+
errorEnvelope(operation, 'INTERNAL', [{ code: 'INTERNAL', message: `${error?.name ?? 'Error'}: unexpected internal failure` }]),
|
|
105
|
+
null,
|
|
106
|
+
2,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function csv(value) {
|
|
112
|
+
return String(value ?? '').split(',').map((item) => item.trim()).filter(Boolean);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function renderEnvelope(value) {
|
|
116
|
+
const clamped = clampEnvelope(value);
|
|
117
|
+
const pretty = JSON.stringify(clamped, null, 2);
|
|
118
|
+
return pretty.length > COMPACT_THRESHOLD ? JSON.stringify(clamped) : pretty;
|
|
119
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { errorEnvelope, okEnvelope } from '@240xu/dsh-tech-lead-core';
|
|
2
|
+
import { parseJsonFields, parseJsonString, renderEnvelope, runGuarded } from '../protocol.js';
|
|
3
|
+
|
|
4
|
+
export function registerContextTools(defineTool, core) {
|
|
5
|
+
const output = [];
|
|
6
|
+
output.push(defineTool({
|
|
7
|
+
name: 'tech_lead_context_validate',
|
|
8
|
+
description: 'Validate an inline tech-lead.context.v1 snapshot. Read-only and deterministic.',
|
|
9
|
+
parameters: { contextJson: { type: 'string', required: true, description: 'context snapshot JSON text' } },
|
|
10
|
+
output: { schema: { type: 'string' }, render: (_a, value) => [{ type: 'text', text: value }] },
|
|
11
|
+
async execute(args) {
|
|
12
|
+
return runGuarded('context_validate', () => {
|
|
13
|
+
const parsed = parseJsonString(args?.contextJson, 'contextJson');
|
|
14
|
+
if (!parsed.ok) return renderEnvelope(errorEnvelope('context_validate', parsed.error.code, [parsed.error]));
|
|
15
|
+
const result = core.validateContext(parsed.value);
|
|
16
|
+
if (!result.valid) {
|
|
17
|
+
const errors = result.errors.map((item) => ({ code: 'SCHEMA_INVALID', path: item.path, message: item.message }));
|
|
18
|
+
return renderEnvelope(errorEnvelope('context_validate', 'SCHEMA_INVALID', errors, result));
|
|
19
|
+
}
|
|
20
|
+
return renderEnvelope(okEnvelope('context_validate', result));
|
|
21
|
+
});
|
|
22
|
+
},
|
|
23
|
+
}));
|
|
24
|
+
output.push(defineTool({
|
|
25
|
+
name: 'tech_lead_evidence_graph_lint',
|
|
26
|
+
description: 'Check explicit evidence references against context ledgers.',
|
|
27
|
+
parameters: { contextJson: { type: 'string', required: true, description: 'context snapshot JSON text' } },
|
|
28
|
+
output: { schema: { type: 'string' }, render: (_a, value) => [{ type: 'text', text: value }] },
|
|
29
|
+
async execute(args) {
|
|
30
|
+
return runGuarded('evidence_graph_lint', () => {
|
|
31
|
+
const parsed = parseJsonString(args?.contextJson, 'contextJson');
|
|
32
|
+
if (!parsed.ok) return renderEnvelope(errorEnvelope('evidence_graph_lint', parsed.error.code, [parsed.error]));
|
|
33
|
+
const result = core.evidenceGraphLint(parsed.value);
|
|
34
|
+
return renderEnvelope(result.valid
|
|
35
|
+
? okEnvelope('evidence_graph_lint', result)
|
|
36
|
+
: errorEnvelope('evidence_graph_lint', 'SCHEMA_INVALID', result.findings, result));
|
|
37
|
+
});
|
|
38
|
+
},
|
|
39
|
+
}));
|
|
40
|
+
output.push(defineTool({
|
|
41
|
+
name: 'tech_lead_evidence_freshness',
|
|
42
|
+
description: 'Detect stale evidence and snapshot fingerprint drift.',
|
|
43
|
+
parameters: {
|
|
44
|
+
contextJson: { type: 'string', required: true, description: 'context snapshot JSON text' },
|
|
45
|
+
optionsJson: { type: 'string', description: 'optional freshness options JSON text (provide now for deterministic runs)' },
|
|
46
|
+
},
|
|
47
|
+
output: { schema: { type: 'string' }, render: (_a, value) => [{ type: 'text', text: value }] },
|
|
48
|
+
async execute(args) {
|
|
49
|
+
return runGuarded('evidence_freshness', () => {
|
|
50
|
+
const input = parseJsonFields(args ?? {}, ['contextJson']);
|
|
51
|
+
if (!input.ok) return renderEnvelope(errorEnvelope('evidence_freshness', 'BAD_INPUT', input.errors));
|
|
52
|
+
let options = {};
|
|
53
|
+
if (args.optionsJson != null && args.optionsJson !== '') {
|
|
54
|
+
const parsedOptions = parseJsonString(args.optionsJson, 'optionsJson');
|
|
55
|
+
if (!parsedOptions.ok) return renderEnvelope(errorEnvelope('evidence_freshness', parsedOptions.error.code, [parsedOptions.error]));
|
|
56
|
+
options = parsedOptions.value && typeof parsedOptions.value === 'object' && !Array.isArray(parsedOptions.value) ? parsedOptions.value : {};
|
|
57
|
+
}
|
|
58
|
+
const clockPinned = typeof options.now === 'string' && Number.isFinite(Date.parse(options.now));
|
|
59
|
+
const result = core.evidenceFreshness(input.values.contextJson, options);
|
|
60
|
+
const envelope = result.stale
|
|
61
|
+
? errorEnvelope('evidence_freshness', 'STALE_EVIDENCE', result.findings, result)
|
|
62
|
+
: okEnvelope('evidence_freshness', result, result.warnings);
|
|
63
|
+
envelope.meta.deterministic = clockPinned;
|
|
64
|
+
return renderEnvelope(envelope);
|
|
65
|
+
});
|
|
66
|
+
},
|
|
67
|
+
}));
|
|
68
|
+
output.push(defineTool({
|
|
69
|
+
name: 'tech_lead_assumption_register',
|
|
70
|
+
description: 'Analyze assumptions and their verification readiness without persisting them.',
|
|
71
|
+
parameters: { contextJson: { type: 'string', required: true, description: 'context snapshot JSON text' } },
|
|
72
|
+
output: { schema: { type: 'string' }, render: (_a, value) => [{ type: 'text', text: value }] },
|
|
73
|
+
async execute(args) {
|
|
74
|
+
return runGuarded('assumption_register', () => {
|
|
75
|
+
const parsed = parseJsonString(args?.contextJson, 'contextJson');
|
|
76
|
+
if (!parsed.ok) return renderEnvelope(errorEnvelope('assumption_register', parsed.error.code, [parsed.error]));
|
|
77
|
+
const assumptions = Array.isArray(parsed.value?.assumptions) ? parsed.value.assumptions : [];
|
|
78
|
+
const items = assumptions.map((item, index) => ({
|
|
79
|
+
id: item?.id ?? `assumption-${index + 1}`,
|
|
80
|
+
status: typeof item?.verification === 'string' && item.verification.trim() ? 'verifiable' : 'missing_verification',
|
|
81
|
+
verification: item?.verification ?? null,
|
|
82
|
+
affects: Array.isArray(item?.affects) ? item.affects.slice() : [],
|
|
83
|
+
}));
|
|
84
|
+
const missing = items.filter((item) => item.status === 'missing_verification');
|
|
85
|
+
if (missing.length) {
|
|
86
|
+
const errors = missing.map((item) => ({ code: 'MISSING_VERIFICATION', path: `/assumptions/${item.id}`, message: 'assumption lacks a verification method' }));
|
|
87
|
+
return renderEnvelope(errorEnvelope('assumption_register', 'SCHEMA_INVALID', errors, { items }));
|
|
88
|
+
}
|
|
89
|
+
return renderEnvelope(okEnvelope('assumption_register', { items }));
|
|
90
|
+
});
|
|
91
|
+
},
|
|
92
|
+
}));
|
|
93
|
+
return output;
|
|
94
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { errorEnvelope, okEnvelope } from '@240xu/dsh-tech-lead-core';
|
|
2
|
+
import { parseJsonFields, renderEnvelope, runGuarded } from '../protocol.js';
|
|
3
|
+
|
|
4
|
+
const FINGERPRINT_KEYS = ['contextFingerprint', 'evidenceFingerprint', 'dependencyFingerprint', 'impactFingerprint'];
|
|
5
|
+
|
|
6
|
+
export function registerGateTools(defineTool, core) {
|
|
7
|
+
const output = [];
|
|
8
|
+
const add = (definition) => output.push(defineTool({ ...definition, output: { schema: { type: 'string' }, render: (_a, value) => [{ type: 'text', text: value }] } }));
|
|
9
|
+
add({
|
|
10
|
+
name: 'tech_lead_gate_plan',
|
|
11
|
+
description: 'Generate deterministic reviewer roles, evidence minimum, quorum, and gate conditions.',
|
|
12
|
+
parameters: {
|
|
13
|
+
impactJson: { type: 'string', required: true, description: 'change impact object as JSON text ({tier?, destructive?})' },
|
|
14
|
+
contextJson: { type: 'string', required: true, description: 'context snapshot JSON text ({tier?})' },
|
|
15
|
+
},
|
|
16
|
+
async execute(args) {
|
|
17
|
+
return runGuarded('gate_plan', () => {
|
|
18
|
+
const input = parseJsonFields(args ?? {}, ['impactJson', 'contextJson']);
|
|
19
|
+
if (!input.ok) return renderEnvelope(errorEnvelope('gate_plan', 'BAD_INPUT', input.errors));
|
|
20
|
+
const errors = [];
|
|
21
|
+
for (const key of ['impactJson', 'contextJson']) {
|
|
22
|
+
const value = input.values[key];
|
|
23
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) errors.push({ code: 'BAD_INPUT', path: key, message: 'expected JSON object' });
|
|
24
|
+
}
|
|
25
|
+
if (errors.length) return renderEnvelope(errorEnvelope('gate_plan', 'BAD_INPUT', errors));
|
|
26
|
+
return renderEnvelope(okEnvelope('gate_plan', core.gatePlan(input.values.impactJson, input.values.contextJson)));
|
|
27
|
+
});
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
add({
|
|
31
|
+
name: 'tech_lead_gate_aggregate',
|
|
32
|
+
description: 'Aggregate anchored role reports, propagate rejects, and de-duplicate findings.',
|
|
33
|
+
parameters: {
|
|
34
|
+
reportsJson: { type: 'string', required: true, description: '[{role,verdict(pass|conditional|reject),anchors:[non-empty strings],findings?[]}] as JSON text; one report per role' },
|
|
35
|
+
planJson: { type: 'string', required: true, description: '{requiredRoles:[...],quorum:<positive int>} as JSON text' },
|
|
36
|
+
},
|
|
37
|
+
async execute(args) {
|
|
38
|
+
return runGuarded('gate_aggregate', () => {
|
|
39
|
+
const input = parseJsonFields(args ?? {}, ['reportsJson', 'planJson']);
|
|
40
|
+
if (!input.ok) return renderEnvelope(errorEnvelope('gate_aggregate', 'BAD_INPUT', input.errors));
|
|
41
|
+
const errors = [];
|
|
42
|
+
if (!Array.isArray(input.values.reportsJson)) errors.push({ code: 'BAD_INPUT', path: 'reportsJson', message: 'expected JSON array' });
|
|
43
|
+
const plan = input.values.planJson;
|
|
44
|
+
if (plan === null || typeof plan !== 'object' || Array.isArray(plan)) {
|
|
45
|
+
errors.push({ code: 'BAD_INPUT', path: 'planJson', message: 'expected JSON object' });
|
|
46
|
+
} else if (!Array.isArray(plan.requiredRoles) || plan.requiredRoles.length === 0 || !Number.isInteger(plan.quorum) || plan.quorum <= 0) {
|
|
47
|
+
errors.push({ code: 'BAD_INPUT', path: 'planJson', message: 'plan needs non-empty requiredRoles and a positive integer quorum' });
|
|
48
|
+
}
|
|
49
|
+
if (errors.length) return renderEnvelope(errorEnvelope('gate_aggregate', 'BAD_INPUT', errors));
|
|
50
|
+
const result = core.gateAggregate(input.values.reportsJson, plan);
|
|
51
|
+
if (!result.pass) {
|
|
52
|
+
const conditionalCount = Array.isArray(input.values.reportsJson)
|
|
53
|
+
? input.values.reportsJson.filter((report) => report && report.verdict === 'conditional').length
|
|
54
|
+
: 0;
|
|
55
|
+
const derived = [
|
|
56
|
+
...result.missingRoles.map((role) => ({ code: 'MISSING_ROLE', path: `/roles/${role}`, message: `no anchored report from required role "${role}"` })),
|
|
57
|
+
...(conditionalCount > 0 ? [{ code: 'CONDITIONAL_VERDICT', path: '/reports', message: `${conditionalCount} report(s) hold a conditional verdict` }] : []),
|
|
58
|
+
...(result.verdict !== 'reject' && result.findings.filter((f) => f.code).length === 0 && Array.isArray(input.values.reportsJson) && input.values.reportsJson.length < plan.quorum
|
|
59
|
+
? [{ code: 'QUORUM_UNMET', path: '/reports', message: `${input.values.reportsJson.length} of ${plan.quorum} reports present` }]
|
|
60
|
+
: []),
|
|
61
|
+
];
|
|
62
|
+
return renderEnvelope(errorEnvelope('gate_aggregate', 'GATE_BLOCKED', [...derived, ...result.findings], result));
|
|
63
|
+
}
|
|
64
|
+
return renderEnvelope(okEnvelope('gate_aggregate', result));
|
|
65
|
+
});
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
add({
|
|
69
|
+
name: 'tech_lead_gate_reopen',
|
|
70
|
+
description: 'Detect whether a previously passed gate must reopen after snapshot drift.',
|
|
71
|
+
parameters: {
|
|
72
|
+
previousJson: { type: 'string', required: true, description: `object with at least one of: ${FINGERPRINT_KEYS.join(', ')}` },
|
|
73
|
+
currentJson: { type: 'string', required: true, description: `object with at least one of: ${FINGERPRINT_KEYS.join(', ')}` },
|
|
74
|
+
},
|
|
75
|
+
async execute(args) {
|
|
76
|
+
return runGuarded('gate_reopen', () => {
|
|
77
|
+
const input = parseJsonFields(args ?? {}, ['previousJson', 'currentJson']);
|
|
78
|
+
if (!input.ok) return renderEnvelope(errorEnvelope('gate_reopen', 'BAD_INPUT', input.errors));
|
|
79
|
+
const errors = [];
|
|
80
|
+
for (const key of ['previousJson', 'currentJson']) {
|
|
81
|
+
const value = input.values[key];
|
|
82
|
+
const usable = value !== null && typeof value === 'object' && !Array.isArray(value) && FINGERPRINT_KEYS.some((k) => value[k] !== undefined);
|
|
83
|
+
if (!usable) errors.push({ code: 'BAD_INPUT', path: key, message: `expected an object containing at least one of: ${FINGERPRINT_KEYS.join(', ')}` });
|
|
84
|
+
}
|
|
85
|
+
if (errors.length) return renderEnvelope(errorEnvelope('gate_reopen', 'BAD_INPUT', errors));
|
|
86
|
+
const result = core.gateReopen(input.values.previousJson, input.values.currentJson);
|
|
87
|
+
const reasons = result.changedInputs.map((item) => ({ code: `${item.toUpperCase()}_DRIFT`, path: `/${item}Fingerprint`, message: `${item} changed` }));
|
|
88
|
+
return renderEnvelope(result.reopen ? errorEnvelope('gate_reopen', 'DRIFT_DETECTED', reasons, result) : okEnvelope('gate_reopen', result));
|
|
89
|
+
});
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
return output;
|
|
93
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { errorEnvelope, previewMutation } from '@240xu/dsh-tech-lead-core';
|
|
2
|
+
import { parseJsonString, renderEnvelope, runGuarded } from '../protocol.js';
|
|
3
|
+
|
|
4
|
+
export function registerMutationTools(defineTool) {
|
|
5
|
+
return [defineTool({
|
|
6
|
+
name: 'tech_lead_mutation_preview',
|
|
7
|
+
description: 'Validate and preview a MutationIntent without executing anything. Schema tech-lead.mutation-intent.v1: mode MUST be "read-only-preview"; requires target[] (each {path,operation}; operations apply/execute/deploy are denied within the bounded marker-scan depth (24 levels), expectedDiff[], recoveryPoint{required:true,...}, verification[] commands-as-inert-strings, authorization{required:true}. Returns CAPABILITY_DENIED for executable modes/markers, SERIALIZATION_FAILED for unserializable payloads.',
|
|
8
|
+
parameters: { intentJson: { type: 'string', required: true, description: 'mutation intent JSON text' } },
|
|
9
|
+
output: { schema: { type: 'string' }, render: (_a, value) => [{ type: 'text', text: value }] },
|
|
10
|
+
async execute(args) {
|
|
11
|
+
return runGuarded('mutation_preview', () => {
|
|
12
|
+
const parsed = parseJsonString(args?.intentJson, 'intentJson');
|
|
13
|
+
if (!parsed.ok) return renderEnvelope(errorEnvelope('mutation_preview', parsed.error.code, [parsed.error]));
|
|
14
|
+
return renderEnvelope(previewMutation(parsed.value));
|
|
15
|
+
});
|
|
16
|
+
},
|
|
17
|
+
})];
|
|
18
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { errorEnvelope, okEnvelope } from '@240xu/dsh-tech-lead-core';
|
|
2
|
+
import { canonicalStringify, parseJsonFields, renderEnvelope, runGuarded } from '../protocol.js';
|
|
3
|
+
|
|
4
|
+
const BLOCKING_FINDINGS = new Set(['CYCLE', 'INVALID_TASK_ID', 'DUPLICATE_TASK_ID']);
|
|
5
|
+
|
|
6
|
+
export function registerProgressTools(defineTool, core) {
|
|
7
|
+
const output = [];
|
|
8
|
+
const register = (name, description, parameters, execute) => output.push(defineTool({ name, description, parameters, output: { schema: { type: 'string' }, render: (_a, value) => [{ type: 'text', text: value }] }, execute }));
|
|
9
|
+
register('tech_lead_progress_decide', 'Decide the next lifecycle outcome (CONTINUE/PAUSE/SCOPE-DOWN/PIVOT/STOP). Trigger fields: dependencies[] pauses when an entry has blocker:true and status!=="done"; evidence[] pauses when an entry has stale:true; gates[] pauses when an entry has destructive:true and status!=="pass". optionsJson supports {forcePivot:true} which returns a PIVOT decision as an error envelope (code PIVOT_REQUESTED) — ok means "no blockers", not "call failed".', {
|
|
10
|
+
contextJson: { type: 'string', required: true, description: 'context snapshot JSON text with dependencies/evidence/gates arrays' },
|
|
11
|
+
optionsJson: { type: 'string', description: 'optional JSON text ({forcePivot?:boolean})' },
|
|
12
|
+
}, async (args) => {
|
|
13
|
+
return runGuarded('progress_decide', () => {
|
|
14
|
+
const input = parseJsonFields(args ?? {}, ['contextJson']);
|
|
15
|
+
if (!input.ok) return renderEnvelope(errorEnvelope('progress_decide', 'BAD_INPUT', input.errors));
|
|
16
|
+
let options = {};
|
|
17
|
+
if (args.optionsJson != null && args.optionsJson !== '') {
|
|
18
|
+
const parsedOptions = parseJsonFields(args, ['optionsJson']);
|
|
19
|
+
if (!parsedOptions.ok) return renderEnvelope(errorEnvelope('progress_decide', 'BAD_INPUT', parsedOptions.errors));
|
|
20
|
+
options = parsedOptions.values.optionsJson;
|
|
21
|
+
}
|
|
22
|
+
const result = core.progressDecide(input.values.contextJson, options);
|
|
23
|
+
return renderEnvelope(result.outcome === 'CONTINUE' ? okEnvelope('progress_decide', result) : errorEnvelope('progress_decide', result.reasons?.[0]?.code ?? 'PROGRESS_BLOCKED', result.reasons, result));
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
register('tech_lead_critical_path', 'Compute blockers, critical path, cycles (with cycleNodes), and parallel windows. Both inputs must be JSON arrays; graph findings (CYCLE / INVALID_TASK_ID / DUPLICATE_TASK_ID) are returned as envelope errors under code SCHEMA_INVALID.', {
|
|
27
|
+
tasksJson: { type: 'string', required: true, description: '[{id,status?,blocker?}] as JSON text' },
|
|
28
|
+
dependenciesJson: { type: 'string', required: true, description: '[{from,to}] edge list as JSON text; from blocks to' },
|
|
29
|
+
}, async (args) => {
|
|
30
|
+
return runGuarded('critical_path', () => {
|
|
31
|
+
const input = parseJsonFields(args ?? {}, ['tasksJson', 'dependenciesJson']);
|
|
32
|
+
if (!input.ok) return renderEnvelope(errorEnvelope('critical_path', 'BAD_INPUT', input.errors));
|
|
33
|
+
const errors = [];
|
|
34
|
+
if (!Array.isArray(input.values.tasksJson)) errors.push({ code: 'BAD_INPUT', path: 'tasksJson', message: 'expected JSON array of tasks' });
|
|
35
|
+
if (!Array.isArray(input.values.dependenciesJson)) errors.push({ code: 'BAD_INPUT', path: 'dependenciesJson', message: 'expected JSON array of edges' });
|
|
36
|
+
if (errors.length) return renderEnvelope(errorEnvelope('critical_path', 'BAD_INPUT', errors));
|
|
37
|
+
const result = core.criticalPath(input.values.tasksJson, input.values.dependenciesJson);
|
|
38
|
+
const blocking = result.findings.filter((f) => BLOCKING_FINDINGS.has(f.code));
|
|
39
|
+
if (blocking.length) {
|
|
40
|
+
return renderEnvelope(errorEnvelope('critical_path', 'SCHEMA_INVALID', result.findings.map((f) => ({ ...f, code: f.code })), result));
|
|
41
|
+
}
|
|
42
|
+
return renderEnvelope(okEnvelope('critical_path', result));
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
register('tech_lead_change_impact', 'Classify change impact (T0/T1/T2), reversibility, and Gate reopen requirements without applying the change. Trigger fields: irreversible (any truthy), publicInterface, modules[], assets[].', {
|
|
46
|
+
changeJson: { type: 'string', required: true, description: '{modules?:string[],assets?:string[],irreversible?,publicInterface?} as JSON text' },
|
|
47
|
+
contextJson: { type: 'string', required: true, description: 'context snapshot JSON text ({gates?:[{id}]})' },
|
|
48
|
+
}, async (args) => {
|
|
49
|
+
return runGuarded('change_impact', () => {
|
|
50
|
+
const input = parseJsonFields(args ?? {}, ['changeJson', 'contextJson']);
|
|
51
|
+
if (!input.ok) return renderEnvelope(errorEnvelope('change_impact', 'BAD_INPUT', input.errors));
|
|
52
|
+
return renderEnvelope(okEnvelope('change_impact', core.changeImpact(input.values.changeJson, input.values.contextJson)));
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
register('tech_lead_resume_reconcile', 'Compare two inline snapshots key-order-insensitively and report deterministic drift with the differing top-level keys.', {
|
|
56
|
+
previousJson: { type: 'string', required: true, description: 'previous snapshot JSON text' },
|
|
57
|
+
currentJson: { type: 'string', required: true, description: 'current snapshot JSON text' },
|
|
58
|
+
}, async (args) => {
|
|
59
|
+
return runGuarded('resume_reconcile', () => {
|
|
60
|
+
const input = parseJsonFields(args ?? {}, ['previousJson', 'currentJson']);
|
|
61
|
+
if (!input.ok) return renderEnvelope(errorEnvelope('resume_reconcile', 'BAD_INPUT', input.errors));
|
|
62
|
+
const previous = canonicalStringify(input.values.previousJson);
|
|
63
|
+
const current = canonicalStringify(input.values.currentJson);
|
|
64
|
+
const changed = JSON.stringify(previous) !== JSON.stringify(current);
|
|
65
|
+
if (!changed) return renderEnvelope(okEnvelope('resume_reconcile', { drift: false }));
|
|
66
|
+
const changedKeys = computeChangedKeys(previous, current);
|
|
67
|
+
return renderEnvelope(errorEnvelope('resume_reconcile', 'DRIFT_DETECTED', changedKeys.map((key) => ({ code: 'KEY_DRIFT', path: `/${key}`, message: `value differs at top-level key "${key}"` })), { drift: true, changedKeys }));
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
return output;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function computeChangedKeys(a, b) {
|
|
74
|
+
if (a === null || b === null || typeof a !== 'object' || typeof b !== 'object') return ['<root>'];
|
|
75
|
+
const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
76
|
+
return [...keys].filter((key) => JSON.stringify(a[key]) !== JSON.stringify(b[key])).sort();
|
|
77
|
+
}
|
package/src/tools.js
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { registerContextTools } from './tools/context.js';
|
|
2
|
+
import { registerProgressTools } from './tools/progress.js';
|
|
3
|
+
import { registerGateTools } from './tools/gates.js';
|
|
4
|
+
import { registerMutationTools } from './tools/mutation.js';
|
|
5
|
+
import { renderEnvelope } from './protocol.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Tool definitions for the tech-lead read-only surface.
|
|
9
|
+
*
|
|
10
|
+
* Every tool computes over caller-supplied primitives/JSON strings:
|
|
11
|
+
* - composite inputs arrive as JSON strings (parsed defensively),
|
|
12
|
+
* - list inputs arrive as comma-separated values,
|
|
13
|
+
* - outputs are pretty-printed JSON strings (uniform string schema); malformed inputs yield structured BAD_INPUT/invalid results instead of throws.
|
|
14
|
+
* - Legacy nine tools return BARE domain shapes (not wrapped in a tech-lead.result.v1 envelope); consumers can discriminate via the absence of meta.schema. Bare top-level finding arrays slice silently at 500 entries (shape preserved, no warning field exists).
|
|
15
|
+
* - All rendered output is clamped: finding/error arrays are capped at 500 entries (FINDINGS_TRUNCATED warning appended), oversized caller-echo arrays collapse into {truncated,total}, and payloads above 256KB switch to compact serialization.
|
|
16
|
+
*
|
|
17
|
+
* No tool touches the filesystem, spawns processes, or performs network I/O.
|
|
18
|
+
*
|
|
19
|
+
* @param {Function} defineTool harness tool factory
|
|
20
|
+
* @param {Record<string, Function>} core pure validators from @240xu/dsh-tech-lead-core
|
|
21
|
+
*/
|
|
22
|
+
export function registerTools(defineTool, core) {
|
|
23
|
+
const json = (str) => {
|
|
24
|
+
try {
|
|
25
|
+
return { ok: true, value: JSON.parse(str) };
|
|
26
|
+
} catch (err) {
|
|
27
|
+
return { ok: false, error: 'invalid JSON: ' + err.message };
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
const csv = (str) =>
|
|
31
|
+
String(str ?? '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
32
|
+
const out = (value) => renderEnvelope(value);
|
|
33
|
+
|
|
34
|
+
/** @type {ReturnType<defineTool>[]} */
|
|
35
|
+
const tools = [];
|
|
36
|
+
|
|
37
|
+
tools.push(defineTool({
|
|
38
|
+
name: 'tech_lead_classify',
|
|
39
|
+
description:
|
|
40
|
+
'Classify a task into tech-lead tiers T0/T1/T2 with reasons. T2 involves multi-module work, irreversible ops, protected assets (user data/secrets/runtime), or public interfaces. Use before planning.',
|
|
41
|
+
parameters: {
|
|
42
|
+
touchesMultipleModules: { type: 'boolean', description: 'change spans ≥2 modules' },
|
|
43
|
+
estimatedDays: { type: 'number', description: 'rough duration in days' },
|
|
44
|
+
irreversibleOps: { type: 'string', description: 'comma-separated irreversible operations' },
|
|
45
|
+
protectedAssetTypes: { type: 'string', description: 'comma-separated among SOURCE,USER_DATA,CONFIG,SECRET,RUNTIME,GENERATED' },
|
|
46
|
+
publicInterfaceChange: { type: 'boolean', description: 'changes public API/contract' },
|
|
47
|
+
uncertainRisk: { type: 'boolean', description: 'risk level cannot be determined yet' },
|
|
48
|
+
},
|
|
49
|
+
output: { schema: { type: 'string' }, render: (_a, v) => [{ type: 'text', text: v }] },
|
|
50
|
+
async execute(args) {
|
|
51
|
+
const provided = [args.touchesMultipleModules, args.estimatedDays, args.irreversibleOps, args.protectedAssetTypes, args.publicInterfaceChange, args.uncertainRisk];
|
|
52
|
+
const input = provided.every((v) => v === undefined || v === '')
|
|
53
|
+
? {}
|
|
54
|
+
: {
|
|
55
|
+
touchesMultipleModules: args.touchesMultipleModules,
|
|
56
|
+
estimatedDays: args.estimatedDays,
|
|
57
|
+
irreversibleOps: csv(args.irreversibleOps),
|
|
58
|
+
protectedAssetTypes: csv(args.protectedAssetTypes),
|
|
59
|
+
publicInterfaceChange: args.publicInterfaceChange,
|
|
60
|
+
uncertainRisk: args.uncertainRisk,
|
|
61
|
+
};
|
|
62
|
+
return out(core.classify(input));
|
|
63
|
+
},
|
|
64
|
+
}));
|
|
65
|
+
|
|
66
|
+
tools.push(defineTool({
|
|
67
|
+
name: 'tech_lead_state_validate',
|
|
68
|
+
description:
|
|
69
|
+
'Validate a tech-lead project state.json (schema v1): enum fields, non-empty anchors on done items, full evidence provenance (id/level E0-E4/source/time/scope/repro). Unknown fields preserved as warnings. Returns pretty-printed JSON string.',
|
|
70
|
+
parameters: {
|
|
71
|
+
stateJson: { type: 'string', required: true, description: 'state.json SERIALIZED AS A STRING — pass the JSON text itself, never a nested object' },
|
|
72
|
+
},
|
|
73
|
+
output: { schema: { type: 'string' }, render: (_a, v) => [{ type: 'text', text: v }] },
|
|
74
|
+
async execute(args) {
|
|
75
|
+
const parsed = json(args.stateJson);
|
|
76
|
+
if (!parsed.ok) return out({ valid: false, errors: [{ path: 'stateJson', message: parsed.error }], warnings: [], unknownFields: [] });
|
|
77
|
+
return out(core.validateState(parsed.value));
|
|
78
|
+
},
|
|
79
|
+
}));
|
|
80
|
+
|
|
81
|
+
tools.push(defineTool({
|
|
82
|
+
name: 'tech_lead_transition_check',
|
|
83
|
+
description:
|
|
84
|
+
'Check whether a proposed outcome transition (CONTINUE/PAUSE/SCOPE-DOWN/PIVOT/STOP) is mechanically justified by the given state. PIVOT needs recorded decisions; SCOPE-DOWN needs goal ledger + risks; STOP needs anchored done items or degraded_reason. Returns pretty-printed JSON string.',
|
|
85
|
+
parameters: {
|
|
86
|
+
stateJson: { type: 'string', required: true, description: 'current state object SERIALIZED AS A STRING (JSON text, not an object)' },
|
|
87
|
+
proposed: { type: 'string', required: true, description: 'one of CONTINUE,PAUSE,SCOPE-DOWN,PIVOT,STOP' },
|
|
88
|
+
},
|
|
89
|
+
output: { schema: { type: 'string' }, render: (_a, v) => [{ type: 'text', text: v }] },
|
|
90
|
+
async execute(args) {
|
|
91
|
+
const parsed = json(args.stateJson);
|
|
92
|
+
if (!parsed.ok) return out({ allowed: false, reason: parsed.error });
|
|
93
|
+
return out(core.transitionCheck(parsed.value, args.proposed));
|
|
94
|
+
},
|
|
95
|
+
}));
|
|
96
|
+
|
|
97
|
+
tools.push(defineTool({
|
|
98
|
+
name: 'tech_lead_plan_lint',
|
|
99
|
+
description:
|
|
100
|
+
'Lint a plan for the tech-lead minimum contracts: goal+metric+target ledger, assumption verification methods, decision alternatives+reasons, risk impacts+mitigations, dependency blockers, rollback for irreversible ops. Returns pretty-printed JSON array of findings.',
|
|
101
|
+
parameters: {
|
|
102
|
+
planJson: { type: 'string', required: true, description: 'plan object SERIALIZED AS A STRING (JSON text, not an object)' },
|
|
103
|
+
},
|
|
104
|
+
output: { schema: { type: 'string' }, render: (_a, v) => [{ type: 'text', text: v }] },
|
|
105
|
+
async execute(args) {
|
|
106
|
+
const parsed = json(args.planJson);
|
|
107
|
+
if (!parsed.ok) return out([{ severity: 'error', path: 'planJson', message: parsed.error }]);
|
|
108
|
+
return out(core.planLint(parsed.value));
|
|
109
|
+
},
|
|
110
|
+
}));
|
|
111
|
+
|
|
112
|
+
tools.push(defineTool({
|
|
113
|
+
name: 'tech_lead_evidence_lint',
|
|
114
|
+
description:
|
|
115
|
+
'Lint evidence entries for complete provenance (id/level E0-E4/source/time/scope/repro). With highRiskChange=true, requires at least one E3+ evidence item. Returns pretty-printed JSON findings array.',
|
|
116
|
+
parameters: {
|
|
117
|
+
evidenceJson: { type: 'string', required: true, description: 'evidence array SERIALIZED AS A STRING (JSON text, not an object)' },
|
|
118
|
+
highRiskChange: { type: 'boolean', description: 'set true for high-risk changes' },
|
|
119
|
+
},
|
|
120
|
+
output: { schema: { type: 'string' }, render: (_a, v) => [{ type: 'text', text: v }] },
|
|
121
|
+
async execute(args) {
|
|
122
|
+
const parsed = json(args.evidenceJson);
|
|
123
|
+
if (!parsed.ok) return out([{ severity: 'error', path: 'evidenceJson', message: parsed.error }]);
|
|
124
|
+
return out(core.evidenceLint(parsed.value, { highRiskChange: args.highRiskChange }));
|
|
125
|
+
},
|
|
126
|
+
}));
|
|
127
|
+
|
|
128
|
+
tools.push(defineTool({
|
|
129
|
+
name: 'tech_lead_gate_precheck',
|
|
130
|
+
description:
|
|
131
|
+
'Precheck a gate review: referee identity separation (proposer/executor must not review), per-report anchors, verdict vocabulary (pass|conditional|reject), solo-review prohibition on destructive scope, blind-gate quorum of ≥3 distinct anchored reviewers. Returns {pass, violations[]}.',
|
|
132
|
+
parameters: {
|
|
133
|
+
inputJson: { type: 'string', required: true, description: '{proposalAuthorId?,executorId?,reviewerIds[],solo?,blindRequired?,destructiveScope[],reports[{reviewerId,verdict,anchors[]}]}' },
|
|
134
|
+
},
|
|
135
|
+
output: { schema: { type: 'string' }, render: (_a, v) => [{ type: 'text', text: v }] },
|
|
136
|
+
async execute(args) {
|
|
137
|
+
const parsed = json(args.inputJson);
|
|
138
|
+
if (!parsed.ok) return out({ pass: false, violations: [{ type: 'BAD_INPUT', detail: parsed.error }] });
|
|
139
|
+
return out(core.gatePrecheck(parsed.value));
|
|
140
|
+
},
|
|
141
|
+
}));
|
|
142
|
+
|
|
143
|
+
tools.push(defineTool({
|
|
144
|
+
name: 'tech_lead_release_audit',
|
|
145
|
+
description:
|
|
146
|
+
'Audit a release set: files outside the allowlist are EXTRA_FILE; when contents are provided, scans lines for absolute home paths, token-like literals (sk-/ghp_/AKIA/xox), credential assignments — each with line numbers. Read-only; never uploads anything. Returns pretty-printed JSON findings array.',
|
|
147
|
+
parameters: {
|
|
148
|
+
allowlistCsv: { type: 'string', required: true, description: 'comma-separated allowed relative paths' },
|
|
149
|
+
filesJson: { type: 'string', required: true, description: '[{path, content?}] as JSON string' },
|
|
150
|
+
contentScan: { type: 'boolean', description: 'default true' },
|
|
151
|
+
},
|
|
152
|
+
output: { schema: { type: 'string' }, render: (_a, v) => [{ type: 'text', text: v }] },
|
|
153
|
+
async execute(args) {
|
|
154
|
+
const parsed = json(args.filesJson);
|
|
155
|
+
if (!parsed.ok) return out([{ type: 'BAD_INPUT', path: 'filesJson', line: 0, detail: parsed.error }]);
|
|
156
|
+
return out(core.releaseAudit({
|
|
157
|
+
allowlist: csv(args.allowlistCsv),
|
|
158
|
+
files: parsed.value,
|
|
159
|
+
contentScan: args.contentScan,
|
|
160
|
+
}));
|
|
161
|
+
},
|
|
162
|
+
}));
|
|
163
|
+
|
|
164
|
+
tools.push(defineTool({
|
|
165
|
+
name: 'tech_lead_install_audit',
|
|
166
|
+
description:
|
|
167
|
+
'Detect install drift between an installed marker manifest and reality: missing managed files, unmanaged extras (backups ignored), version mismatch against the package. Returns {missingManaged[], unmanaged[], versionMismatch, newInPackage[]}.',
|
|
168
|
+
parameters: {
|
|
169
|
+
manifestJson: { type: 'string', required: true, description: 'marker {version, files[]} SERIALIZED AS A STRING (JSON text, not an object)' },
|
|
170
|
+
actualFilesCsv: { type: 'string', required: true, description: 'comma-separated relative paths present under target' },
|
|
171
|
+
pkgFilesCsv: { type: 'string', required: true, description: 'comma-separated managed paths in current package' },
|
|
172
|
+
pkgVersion: { type: 'string', required: true, description: 'package version string' },
|
|
173
|
+
},
|
|
174
|
+
output: { schema: { type: 'string' }, render: (_a, v) => [{ type: 'text', text: v }] },
|
|
175
|
+
async execute(args) {
|
|
176
|
+
const parsed = json(args.manifestJson);
|
|
177
|
+
if (!parsed.ok || !parsed.value || !Array.isArray(parsed.value.files)) {
|
|
178
|
+
return out({ missingManaged: [], unmanaged: [], versionMismatch: false, newInPackage: [], error: 'manifestJson must be {version, files[]}' });
|
|
179
|
+
}
|
|
180
|
+
return out(core.installAudit(
|
|
181
|
+
parsed.value,
|
|
182
|
+
csv(args.actualFilesCsv),
|
|
183
|
+
csv(args.pkgFilesCsv),
|
|
184
|
+
args.pkgVersion
|
|
185
|
+
));
|
|
186
|
+
},
|
|
187
|
+
}));
|
|
188
|
+
|
|
189
|
+
tools.push(defineTool({
|
|
190
|
+
name: 'tech_lead_resume_card',
|
|
191
|
+
description:
|
|
192
|
+
'Render a three-line resume card from a tech-lead state: position (tier/phase/mode), last outcome, next step — plus stale-evidence detection (>maxAgeDays old) and warnings for empty next_step or open gates. Returns pretty-printed JSON card.',
|
|
193
|
+
parameters: {
|
|
194
|
+
stateJson: { type: 'string', required: true, description: 'state object SERIALIZED AS A STRING (JSON text, not an object)' },
|
|
195
|
+
nowIso: { type: 'string', description: 'reference time ISO string (defaults to real now)' },
|
|
196
|
+
maxAgeDays: { type: 'number', description: 'stale threshold in days, default 7' },
|
|
197
|
+
},
|
|
198
|
+
output: { schema: { type: 'string' }, render: (_a, v) => [{ type: 'text', text: v }] },
|
|
199
|
+
async execute(args) {
|
|
200
|
+
const parsed = json(args.stateJson);
|
|
201
|
+
if (!parsed.ok) {
|
|
202
|
+
return out({ position: '?', lastGate: '?', nextStep: '(invalid state)', staleEvidenceIds: [], warnings: [parsed.error] });
|
|
203
|
+
}
|
|
204
|
+
return out(core.resumeCard(parsed.value, { now: args.nowIso, maxAgeDays: args.maxAgeDays }));
|
|
205
|
+
},
|
|
206
|
+
}));
|
|
207
|
+
|
|
208
|
+
if (core.validateContext && core.evidenceGraphLint && core.evidenceFreshness) {
|
|
209
|
+
// Domain registration stays optional so the legacy nine-tool contract can
|
|
210
|
+
// be reused by callers that supply only the original core surface.
|
|
211
|
+
tools.push(...registerContextTools(defineTool, core));
|
|
212
|
+
}
|
|
213
|
+
if (core.progressDecide && core.criticalPath && core.changeImpact) {
|
|
214
|
+
tools.push(...registerProgressTools(defineTool, core));
|
|
215
|
+
}
|
|
216
|
+
if (core.gatePlan && core.gateAggregate && core.gateReopen) {
|
|
217
|
+
tools.push(...registerGateTools(defineTool, core));
|
|
218
|
+
}
|
|
219
|
+
if (core.previewMutation) tools.push(...registerMutationTools(defineTool));
|
|
220
|
+
return tools;
|
|
221
|
+
}
|