@antoneeo/mkt-agentic-sdlc-skill 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/scripts/init.js +3 -10
- package/scripts/lib.js +233 -183
- package/skills/mkt-agentic-sdlc/research.md +7 -1
- package/skills/mkt-agentic-sdlc/review.md +12 -0
- package/skills/mkt-agentic-sdlc/scripts/mkt_check.py +26 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.1 — 2026-08-02
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **The multi-lens routing note announced this skill as the code lens.** `init.js` wrote
|
|
7
|
+
the row for the CURRENT lens as a hardcoded literal copied from the code distribution,
|
|
8
|
+
so a project initialised with `mkt-sdlc-init` listed `agentic-sdlc` twice and never
|
|
9
|
+
named `mkt-agentic-sdlc`. Both the self row and the sibling table are now derived from
|
|
10
|
+
the shared `routing.md` lens table. Found by a cold-agent field test.
|
|
11
|
+
- **`FACT` was the way past the evidence rule.** `BENCHMARK` owes a source URL and
|
|
12
|
+
`ASSUMPTION` a range; `FACT` owed nothing, so a researched observation classed FACT
|
|
13
|
+
with source "see research/VOC.md" entered the ledger unsourced. `mkt_check.py` now
|
|
14
|
+
errors on a FACT with no source, or one pointing at a document of the engagement,
|
|
15
|
+
while exempting a cell that names the client as origin. `research.md` states the two
|
|
16
|
+
legal origins.
|
|
17
|
+
- `review.md` (shared spine): a review verdict travels as the reviewer's final output.
|
|
18
|
+
|
|
19
|
+
## 0.3.0 — 2026-08-01
|
|
20
|
+
|
|
21
|
+
- **One shared core, three lenses.** This skill is now built from the family's spine
|
|
22
|
+
instead of a copy of it: triage, the Vision gate, the review gates, the guide router,
|
|
23
|
+
question discipline and the validator core are byte-identical with
|
|
24
|
+
`@antoneeo/agentic-sdlc-skill` and `@antoneeo/kb-agentic-skill`, and a drift guard
|
|
25
|
+
verifies it on every build. `routing.md` decides which lens owns a unit of work when
|
|
26
|
+
more than one is installed.
|
|
27
|
+
- `mkt_check.py` forwards every spine subcommand it does not intercept by iterating the
|
|
28
|
+
spine's parser — the hand-copied command tuple used to drop `migrate` while `SKILL.md`
|
|
29
|
+
documented it.
|
|
30
|
+
|
|
3
31
|
## 0.2.1 — 2026-07-09
|
|
4
32
|
|
|
5
33
|
- **Install DX fix**: README + the generated project protocol pointer no longer present `mkt-sdlc-install-skill` as a required second step. `npm i -g` alone installs the skill (the `postinstall` runs the installer); the manual command is documented as a fallback via `npx mkt-sdlc-install-skill` for setups that block install scripts, with an explicit note that the bare command is only on PATH after a global install. Prevents the "command not recognized" error after a local `npm i`.
|
package/gemini-extension.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mkt-agentic-sdlc",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Evidence-First marketing planning protocol with engagement triage, SOSTAC workflow, evidence ledger and optional devPNT integration.",
|
|
5
5
|
"author": "Antonio Pinto (https://github.com/Antoneeo)"
|
|
6
6
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@antoneeo/mkt-agentic-sdlc-skill",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Evidence-First marketing planning protocol for Claude Code, Gemini CLI, Google Antigravity and Codex: engagement triage, SOSTAC workflow, evidence ledger, mechanical validator and optional devPNT integration.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|
package/scripts/init.js
CHANGED
|
@@ -3,14 +3,7 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const { execSync } = require('child_process');
|
|
6
|
-
const { SKILL_SOURCE, CLIENTS, clientDetected, skillTarget, loadTemplates, templateFor } = require('./lib');
|
|
7
|
-
|
|
8
|
-
// Sibling lenses of the same family: one shared core, one `ai_docs/` tree, a different
|
|
9
|
-
// fidelity discipline each. Keyed by the installed skill directory name.
|
|
10
|
-
const SIBLING_LENSES = {
|
|
11
|
-
'kb-agentic': 'knowledge',
|
|
12
|
-
'agentic-sdlc': 'code',
|
|
13
|
-
};
|
|
6
|
+
const { SKILL_SOURCE, INSTALLED_SKILL_NAME, SELF_LENS, SIBLING_LENSES, CLIENTS, clientDetected, skillTarget, loadTemplates, templateFor } = require('./lib');
|
|
14
7
|
|
|
15
8
|
const cwd = process.cwd();
|
|
16
9
|
|
|
@@ -159,7 +152,7 @@ if (siblings.size > 0) {
|
|
|
159
152
|
This project has more than one lens of the Agentic SDLC family installed:
|
|
160
153
|
|
|
161
154
|
${list}
|
|
162
|
-
- \`
|
|
155
|
+
- \`${INSTALLED_SKILL_NAME}\` — the **${SELF_LENS}** lens
|
|
163
156
|
|
|
164
157
|
One \`ai_docs/\` tree, one project default (\`default_domain:\` in \`ai_docs/README.md\`),
|
|
165
158
|
one lens per unit of work. Before acting on any L2, L3 or Spike, run the domain router
|
|
@@ -186,7 +179,7 @@ Delete it once the merge is done.
|
|
|
186
179
|
`, 'multi-lens routing note');
|
|
187
180
|
if (wrote && protocolPreexisting) {
|
|
188
181
|
console.log(' ⚠️ A protocol pointer already existed and was NOT overwritten.');
|
|
189
|
-
console.log(
|
|
182
|
+
console.log(` Merge the ${SELF_LENS}-lens ladder from AGENTIC_MULTI_LENS.md into it by hand.`);
|
|
190
183
|
}
|
|
191
184
|
}
|
|
192
185
|
|
package/scripts/lib.js
CHANGED
|
@@ -1,183 +1,233 @@
|
|
|
1
|
-
// Shared helpers for the Marketing Agentic SDLC npm scripts (init / postinstall / preuninstall).
|
|
2
|
-
// Single source for client detection and skill-target paths: init and postinstall
|
|
3
|
-
// must never disagree on what "Claude Code is installed" means.
|
|
4
|
-
|
|
5
|
-
const fs = require('fs');
|
|
6
|
-
const path = require('path');
|
|
7
|
-
const os = require('os');
|
|
8
|
-
const { execSync } = require('child_process');
|
|
9
|
-
|
|
10
|
-
const PACKAGE_ROOT = path.resolve(__dirname, '..');
|
|
11
|
-
const SKILL_SOURCE = path.join(PACKAGE_ROOT, 'skills', 'mkt-agentic-sdlc');
|
|
12
|
-
const TEMPLATES_PATH = path.join(SKILL_SOURCE, 'templates.md');
|
|
13
|
-
// Kept for backward compatibility with this package's own scripts; the
|
|
14
|
-
// authoritative value is INSTALLED_SKILL_NAME, derived from SKILL.md below.
|
|
15
|
-
// The directory name each client loads the skill from. Derived from the manifest,
|
|
16
|
-
// never hard-coded by a consumer: three distributions share these scripts, and a
|
|
17
|
-
// literal here is how a copy-fork starts installing under its sibling's name.
|
|
18
|
-
const INSTALLED_SKILL_NAME = (() => {
|
|
19
|
-
const m = fs.readFileSync(path.join(SKILL_SOURCE, 'SKILL.md'), 'utf8').match(/^name:\s*(\S+)/m);
|
|
20
|
-
if (!m) throw new Error(`SKILL.md carries no 'name:' field: ${SKILL_SOURCE}`);
|
|
21
|
-
return m[1];
|
|
22
|
-
})();
|
|
23
|
-
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
{
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
1
|
+
// Shared helpers for the Marketing Agentic SDLC npm scripts (init / postinstall / preuninstall).
|
|
2
|
+
// Single source for client detection and skill-target paths: init and postinstall
|
|
3
|
+
// must never disagree on what "Claude Code is installed" means.
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
const { execSync } = require('child_process');
|
|
9
|
+
|
|
10
|
+
const PACKAGE_ROOT = path.resolve(__dirname, '..');
|
|
11
|
+
const SKILL_SOURCE = path.join(PACKAGE_ROOT, 'skills', 'mkt-agentic-sdlc');
|
|
12
|
+
const TEMPLATES_PATH = path.join(SKILL_SOURCE, 'templates.md');
|
|
13
|
+
// Kept for backward compatibility with this package's own scripts; the
|
|
14
|
+
// authoritative value is INSTALLED_SKILL_NAME, derived from SKILL.md below.
|
|
15
|
+
// The directory name each client loads the skill from. Derived from the manifest,
|
|
16
|
+
// never hard-coded by a consumer: three distributions share these scripts, and a
|
|
17
|
+
// literal here is how a copy-fork starts installing under its sibling's name.
|
|
18
|
+
const INSTALLED_SKILL_NAME = (() => {
|
|
19
|
+
const m = fs.readFileSync(path.join(SKILL_SOURCE, 'SKILL.md'), 'utf8').match(/^name:\s*(\S+)/m);
|
|
20
|
+
if (!m) throw new Error(`SKILL.md carries no 'name:' field: ${SKILL_SOURCE}`);
|
|
21
|
+
return m[1];
|
|
22
|
+
})();
|
|
23
|
+
|
|
24
|
+
// The family's lens table, read from the shared `routing.md` rather than restated
|
|
25
|
+
// here. Same reason as INSTALLED_SKILL_NAME above, and the same failure it prevents:
|
|
26
|
+
// BOTH the row for this lens and the rows for its siblings used to be literals copied
|
|
27
|
+
// between distributions, so kb and mkt wrote a multi-lens note announcing themselves
|
|
28
|
+
// as the code lens. routing.md is one of the byte-identical shared files, so this
|
|
29
|
+
// lookup cannot drift between distributions.
|
|
30
|
+
// Lazy on purpose: preuninstall.js needs INSTALLED_SKILL_NAME and nothing else, and
|
|
31
|
+
// must keep working on an installation damaged badly enough to have lost routing.md.
|
|
32
|
+
function lensTable() {
|
|
33
|
+
const routing = path.join(SKILL_SOURCE, 'routing.md');
|
|
34
|
+
if (!fs.existsSync(routing)) throw new Error(`routing.md missing from the skill: ${SKILL_SOURCE}`);
|
|
35
|
+
// Parsed as a table, not matched with a regex: a mis-escaped pattern matches the
|
|
36
|
+
// empty string and yields undefined instead of throwing, which is how this lookup
|
|
37
|
+
// failed the first time it was written.
|
|
38
|
+
const table = new Map();
|
|
39
|
+
for (const line of fs.readFileSync(routing, 'utf8').split(/\r?\n/)) {
|
|
40
|
+
const cells = line.split('|').map((cell) => cell.trim());
|
|
41
|
+
if (cells.length < 4) continue;
|
|
42
|
+
const [, lens, skill] = cells;
|
|
43
|
+
if (!/^[a-z]+$/.test(lens) || !/^`[a-z0-9-]+`$/.test(skill)) continue;
|
|
44
|
+
const name = skill.slice(1, -1);
|
|
45
|
+
// A duplicate row would otherwise be won silently by whichever came first, and a
|
|
46
|
+
// wrong routing.md propagates byte-identically to every distribution.
|
|
47
|
+
if (table.has(name)) throw new Error(`routing.md lists '${name}' more than once`);
|
|
48
|
+
table.set(name, lens);
|
|
49
|
+
}
|
|
50
|
+
if (!table.size) throw new Error(`routing.md carries no lens table: ${routing}`);
|
|
51
|
+
return table;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function selfLens() {
|
|
55
|
+
const lens = lensTable().get(INSTALLED_SKILL_NAME);
|
|
56
|
+
if (!lens) throw new Error(`routing.md has no lens row for '${INSTALLED_SKILL_NAME}'`);
|
|
57
|
+
return lens;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Sibling lenses of the same family: one shared core, one docs tree, a different
|
|
61
|
+
// fidelity discipline each. Keyed by the installed skill directory name.
|
|
62
|
+
function siblingLenses() {
|
|
63
|
+
const table = lensTable();
|
|
64
|
+
selfLens(); // this lens must be in the table too
|
|
65
|
+
table.delete(INSTALLED_SKILL_NAME);
|
|
66
|
+
return Object.fromEntries(table);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// One entry per supported AI client. `home` may be overridden by an env var
|
|
70
|
+
// (Claude Desktop / portable installs); presence of the home dir counts as
|
|
71
|
+
// detection even when the CLI is not on PATH.
|
|
72
|
+
|
|
73
|
+
// One entry per supported AI client. `home` may be overridden by an env var
|
|
74
|
+
// (Claude Desktop / portable installs); presence of the home dir counts as
|
|
75
|
+
// detection even when the CLI is not on PATH.
|
|
76
|
+
const CLIENTS = [
|
|
77
|
+
{
|
|
78
|
+
key: 'claude',
|
|
79
|
+
label: 'Claude Code',
|
|
80
|
+
cmd: 'claude',
|
|
81
|
+
home: process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude'),
|
|
82
|
+
envVar: 'CLAUDE_CONFIG_DIR',
|
|
83
|
+
reload: 'Restart Claude Code to load it. Invoke via Skill tool as "mkt-agentic-sdlc".',
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
key: 'gemini',
|
|
87
|
+
label: 'Gemini CLI',
|
|
88
|
+
cmd: 'gemini',
|
|
89
|
+
home: process.env.GEMINI_HOME || path.join(os.homedir(), '.gemini'),
|
|
90
|
+
envVar: 'GEMINI_HOME',
|
|
91
|
+
reload: 'Run "gemini skills reload" or restart Gemini CLI to load it.',
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
key: 'codex',
|
|
95
|
+
label: 'Codex AI',
|
|
96
|
+
cmd: 'codex',
|
|
97
|
+
home: process.env.CODEX_HOME || path.join(os.homedir(), '.codex'),
|
|
98
|
+
envVar: 'CODEX_HOME',
|
|
99
|
+
reload: 'Restart Codex to load it. Invoke it as "$mkt-agentic-sdlc" or by asking for the Marketing SDLC skill.',
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
// Google Antigravity 2.0 discovers global agent skills under
|
|
103
|
+
// ~/.gemini/config/skills/ -- the SAME home the legacy Gemini CLI claims.
|
|
104
|
+
// To avoid a shared-home double-install, this entry sets:
|
|
105
|
+
// - skillsSubdir 'config/skills': distinct target from gemini's ~/.gemini/skills
|
|
106
|
+
// - homeMarker on ~/.gemini/config/skills: detection never fires on bare
|
|
107
|
+
// ~/.gemini (which every Antigravity user has); only the Antigravity skills
|
|
108
|
+
// dir, the `agy` CLI, or ANTIGRAVITY_HOME count as "Antigravity installed".
|
|
109
|
+
key: 'antigravity',
|
|
110
|
+
label: 'Google Antigravity',
|
|
111
|
+
cmd: 'agy',
|
|
112
|
+
home: process.env.ANTIGRAVITY_HOME || path.join(os.homedir(), '.gemini'),
|
|
113
|
+
envVar: 'ANTIGRAVITY_HOME',
|
|
114
|
+
skillsSubdir: 'config/skills',
|
|
115
|
+
homeMarker: path.join(
|
|
116
|
+
process.env.ANTIGRAVITY_HOME || path.join(os.homedir(), '.gemini'),
|
|
117
|
+
'config',
|
|
118
|
+
'skills',
|
|
119
|
+
),
|
|
120
|
+
reload: 'Restart Antigravity, or run "agy skills reload", to load it. Invoke by asking for the Marketing SDLC skill.',
|
|
121
|
+
},
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
function commandExists(cmd) {
|
|
125
|
+
try {
|
|
126
|
+
execSync(`${cmd} --version`, { stdio: 'ignore' });
|
|
127
|
+
return true;
|
|
128
|
+
} catch (e) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function clientDetected(client) {
|
|
134
|
+
// An entry may override the fs-existence probe with a `homeMarker` (a more
|
|
135
|
+
// specific path than the bare home) so two clients sharing a home dir do not
|
|
136
|
+
// both fire on its mere existence. Entries without a marker check `home`
|
|
137
|
+
// exactly as before (backward-compatible).
|
|
138
|
+
const homePathToCheck = client.homeMarker || client.home;
|
|
139
|
+
return commandExists(client.cmd)
|
|
140
|
+
|| Boolean(process.env[client.envVar])
|
|
141
|
+
|| fs.existsSync(homePathToCheck);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function skillTarget(client) {
|
|
145
|
+
// An entry may override the default `skills` sub-path with `skillsSubdir`
|
|
146
|
+
// (split on '/' to keep cross-platform path.join correctness). Entries
|
|
147
|
+
// without it resolve to <home>/skills/mkt-agentic-sdlc exactly as before.
|
|
148
|
+
const subdir = client.skillsSubdir ? client.skillsSubdir.split('/') : ['skills'];
|
|
149
|
+
return path.join(client.home, ...subdir, INSTALLED_SKILL_NAME);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function copyRecursive(src, dest) {
|
|
153
|
+
if (typeof fs.cpSync === 'function') {
|
|
154
|
+
fs.cpSync(src, dest, { recursive: true, force: true });
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
// Fallback for Node < 16.7
|
|
158
|
+
if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
|
|
159
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
160
|
+
const s = path.join(src, entry.name);
|
|
161
|
+
const d = path.join(dest, entry.name);
|
|
162
|
+
if (entry.isDirectory()) copyRecursive(s, d);
|
|
163
|
+
else fs.copyFileSync(s, d);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Parse templates.md into { headingText: [fencedBlock, ...] }.
|
|
169
|
+
* Templates are single-sourced there: the init script must extract them
|
|
170
|
+
* instead of carrying its own inline copies (which historically drifted).
|
|
171
|
+
*/
|
|
172
|
+
function loadTemplates() {
|
|
173
|
+
const text = fs.readFileSync(TEMPLATES_PATH, 'utf8');
|
|
174
|
+
const lines = text.split(/\r?\n/);
|
|
175
|
+
const sections = {};
|
|
176
|
+
let heading = null;
|
|
177
|
+
let block = null;
|
|
178
|
+
for (const line of lines) {
|
|
179
|
+
const h = line.match(/^##\s+(.*)$/);
|
|
180
|
+
if (h && block === null) {
|
|
181
|
+
heading = h[1].trim();
|
|
182
|
+
sections[heading] = sections[heading] || [];
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (/^```/.test(line)) {
|
|
186
|
+
if (block === null) {
|
|
187
|
+
block = [];
|
|
188
|
+
} else {
|
|
189
|
+
if (heading) sections[heading].push(block.join('\n') + '\n');
|
|
190
|
+
block = null;
|
|
191
|
+
}
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (block !== null) block.push(line);
|
|
195
|
+
}
|
|
196
|
+
return sections;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Return the Nth fenced block of the section whose heading contains `needle`.
|
|
201
|
+
* Throws with a clear message when missing: writing a wrong or empty
|
|
202
|
+
* boilerplate silently would be worse than failing the init.
|
|
203
|
+
*/
|
|
204
|
+
function templateFor(sections, needle, index = 0) {
|
|
205
|
+
const heading = Object.keys(sections).find((h) => h.includes(needle));
|
|
206
|
+
const blocks = heading ? sections[heading] : undefined;
|
|
207
|
+
if (!blocks || !blocks[index]) {
|
|
208
|
+
throw new Error(
|
|
209
|
+
`Template section containing "${needle}" (block ${index}) not found in ${TEMPLATES_PATH}. ` +
|
|
210
|
+
'The package is corrupted or templates.md was restructured: fix templates.md, do not improvise content.'
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
return blocks[index];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
module.exports = {
|
|
217
|
+
INSTALLED_SKILL_NAME,
|
|
218
|
+
PACKAGE_ROOT,
|
|
219
|
+
SKILL_SOURCE,
|
|
220
|
+
TEMPLATES_PATH,
|
|
221
|
+
CLIENTS,
|
|
222
|
+
commandExists,
|
|
223
|
+
clientDetected,
|
|
224
|
+
skillTarget,
|
|
225
|
+
copyRecursive,
|
|
226
|
+
loadTemplates,
|
|
227
|
+
templateFor,
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
// Lazy: reading routing.md is deferred to the consumer that actually asks.
|
|
231
|
+
Object.defineProperty(module.exports, 'SELF_LENS', { enumerable: true, get: selfLens });
|
|
232
|
+
Object.defineProperty(module.exports, 'SIBLING_LENSES', { enumerable: true, get: siblingLenses });
|
|
233
|
+
|
|
@@ -16,7 +16,13 @@ piece of evidence:
|
|
|
16
16
|
numbers as `[EV-nn]`.
|
|
17
17
|
- **Class** (exactly one):
|
|
18
18
|
- `FACT` — user input or primary data (their analytics, their CRM export).
|
|
19
|
-
Source = "user, Wave 1" or the data file.
|
|
19
|
+
Source = "user, Wave 1" or the data file. **Only these two origins.** An
|
|
20
|
+
observation *you* gathered — a forum quote, a competitor's page, a review
|
|
21
|
+
site — is a `BENCHMARK` however solid it looks, and owes its URL. FACT is
|
|
22
|
+
the one class the URL rule cannot reach, so classing research as FACT is
|
|
23
|
+
the way an unsourced number gets in wearing the strongest label; the
|
|
24
|
+
validator now rejects a FACT whose Source names one of your own documents
|
|
25
|
+
or says "see X".
|
|
20
26
|
- `BENCHMARK` — external number. Source URL **and** publication date
|
|
21
27
|
mandatory. The Source cell must carry the actual external URL, NOT a
|
|
22
28
|
pointer to one of your own research documents that aggregates it ("see
|
|
@@ -85,6 +85,14 @@ When you hand work to a reviewer (human or agent), give them:
|
|
|
85
85
|
should have been. The design reviewer checks that every threat surface the change
|
|
86
86
|
touches has a matching security requirement.
|
|
87
87
|
|
|
88
|
+
**The verdict travels back as the reviewer's own final output** — the text it
|
|
89
|
+
returns when it finishes, nothing else. A reviewer that tries to message the
|
|
90
|
+
requester mid-run depends on a delivery channel it cannot verify (a subagent
|
|
91
|
+
addressed by agent TYPE rather than by session gets no such channel, and the
|
|
92
|
+
attempt fails silently); a requester that waits for such a message stalls
|
|
93
|
+
holding a verdict that already exists. State the return form when you request
|
|
94
|
+
the review, and read the verdict where it actually arrives.
|
|
95
|
+
|
|
88
96
|
Never ask a reviewer to "review my session" or "review what I just did"
|
|
89
97
|
without the artifacts above — that forces them to reconstruct scope from
|
|
90
98
|
conversation instead of reviewing the change itself. Say which finding
|
|
@@ -109,6 +117,10 @@ When you are the reviewer:
|
|
|
109
117
|
|
|
110
118
|
- Verify claims against the real source, not against the diff's own
|
|
111
119
|
description of itself.
|
|
120
|
+
- **Your verdict is your final output.** Deliver findings and verdict as the text
|
|
121
|
+
you return when you finish — never only through a message to the requester, a
|
|
122
|
+
channel you cannot verify and which fails silently when it is not there
|
|
123
|
+
(see `## Requesting`).
|
|
112
124
|
- Cite evidence as `file:line` for every finding — a finding without a
|
|
113
125
|
location is not actionable.
|
|
114
126
|
- Keep severity honest: do not inflate a style preference to a blocker, and
|
|
@@ -84,6 +84,18 @@ VALID_STATUS = ("CURRENT", "SUPERSEDED", "DRAFT", "DEPRECATED")
|
|
|
84
84
|
LEDGER_CLASSES = ("FACT", "BENCHMARK", "ASSUMPTION")
|
|
85
85
|
CONFIDENCE_VALUES = ("HIGH", "MED", "MEDIUM", "LOW")
|
|
86
86
|
EV_REF = re.compile(r"\[(EV-\d+)\]")
|
|
87
|
+
# A source that points INSIDE this engagement instead of at the world: a document of
|
|
88
|
+
# OUR OWN under a canonical dir, or the "see X" form. Only FACT can carry it today,
|
|
89
|
+
# because FACT is the one class the URL rule does not reach.
|
|
90
|
+
INTERNAL_SOURCE_RE = re.compile(
|
|
91
|
+
r"(?:\b(?:mkt_docs|ai_docs|research|strategy|tactics|deliverables|vision)/\S*\.md\b"
|
|
92
|
+
r"|\b(?:see|vedi|cfr\.?)\s)", re.IGNORECASE)
|
|
93
|
+
# The two origins research.md sanctions for a FACT. A cell that opens by naming the
|
|
94
|
+
# client is exempt: their own primary data may well be a Markdown file they handed
|
|
95
|
+
# over, and telling its author to "reclassify as BENCHMARK and add a URL" would be
|
|
96
|
+
# advice to misclassify.
|
|
97
|
+
CLIENT_ORIGIN_RE = re.compile(
|
|
98
|
+
r"^\s*(?:user|client|owner|utente|cliente|titolare|proprietario)\b", re.IGNORECASE)
|
|
87
99
|
OBJECTIVE_HEADING = re.compile(r"^###\s+(O\d+)\b", re.MULTILINE)
|
|
88
100
|
OBJECTIVE_TOKEN = re.compile(r"\bO\d+\b")
|
|
89
101
|
EXAMPLE_MARKER = "(example)"
|
|
@@ -370,6 +382,20 @@ def run_ledger(root):
|
|
|
370
382
|
rep.error(f"ledger {rid}: BENCHMARK without a source URL")
|
|
371
383
|
if not date:
|
|
372
384
|
rep.error(f"ledger {rid}: BENCHMARK without a date")
|
|
385
|
+
if cls == "FACT" and not source:
|
|
386
|
+
rep.error(f"ledger {rid}: FACT without a source -- name the origin "
|
|
387
|
+
"('user, Wave 1' or the client's data file)")
|
|
388
|
+
if (cls == "FACT" and source and not CLIENT_ORIGIN_RE.match(source)
|
|
389
|
+
and INTERNAL_SOURCE_RE.search(source)):
|
|
390
|
+
# FACT means "the client told me" or "the client's own primary data" — the
|
|
391
|
+
# one class that legitimately has no URL. Pointing it at another document
|
|
392
|
+
# of this engagement is how a researched observation gets in without the
|
|
393
|
+
# URL its class would owe: the row cites a file that cites the source, the
|
|
394
|
+
# validator resolves nothing, and "see VOC.md" is exactly the form the
|
|
395
|
+
# BENCHMARK rule rejects. Observed in a cold-agent field test, 2026-08-02.
|
|
396
|
+
rep.error(f"ledger {rid}: FACT sourced to an internal document ('{source}') -- "
|
|
397
|
+
"a FACT is what the client stated or their own primary data; an "
|
|
398
|
+
"observation gathered by research is a BENCHMARK and owes its URL")
|
|
373
399
|
if cls == "ASSUMPTION":
|
|
374
400
|
if confidence not in CONFIDENCE_VALUES:
|
|
375
401
|
rep.error(f"ledger {rid}: ASSUMPTION without confidence (HIGH|MED|LOW)")
|