@donghyeonlee/jjamppong-harness 0.1.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.
Files changed (62) hide show
  1. package/AGENTS.md +77 -0
  2. package/CONTEXT.md +51 -0
  3. package/README.md +85 -0
  4. package/bin/jjamppong.js +123 -0
  5. package/handoff.md +13 -0
  6. package/harness/contracts/capability-catalog.yaml +128 -0
  7. package/harness/contracts/gate-contract-matrix.yaml +188 -0
  8. package/harness/contracts/installer-contract.yaml +79 -0
  9. package/harness/contracts/ledger-event.schema.yaml +79 -0
  10. package/harness/contracts/path-policy.schema.yaml +51 -0
  11. package/harness/contracts/permission-decision.schema.yaml +95 -0
  12. package/harness/contracts/task.schema.yaml +88 -0
  13. package/harness/docs/adr/.gitkeep +1 -0
  14. package/harness/docs/adr/2026-06-02-jjamppong-planning-gate.md +33 -0
  15. package/harness/docs/agents/domain.md +14 -0
  16. package/harness/docs/agents/issue-tracker.md +9 -0
  17. package/harness/docs/agents/matt-pocock-skills.md +60 -0
  18. package/harness/docs/agents/triage-labels.md +11 -0
  19. package/harness/docs/solutions/.gitkeep +1 -0
  20. package/harness/docs/tasks/active/.gitkeep +1 -0
  21. package/harness/docs/tasks/archive/.gitkeep +1 -0
  22. package/harness/docs/tasks/archive/index.md +5 -0
  23. package/harness/docs/tasks/index.md +13 -0
  24. package/harness/doctor/doctor.js +114 -0
  25. package/harness/installer/install.js +235 -0
  26. package/harness/lifecycle/lifecycle.js +133 -0
  27. package/harness/permission/permission-decision.js +377 -0
  28. package/harness/release/CHECKSUMS.sha256 +84 -0
  29. package/harness/release/RELEASE-NOTES.md +33 -0
  30. package/harness/release/SOURCE-MANIFEST.md +31 -0
  31. package/harness/rules/module-types.md +98 -0
  32. package/harness/rules/rules.md +220 -0
  33. package/harness/rules/workflow.md +252 -0
  34. package/harness/state/compound.md +7 -0
  35. package/harness/state/intake.md +11 -0
  36. package/harness/state/module-structure.md +13 -0
  37. package/harness/state/planning.md +21 -0
  38. package/harness/templates/module/module-state.md +13 -0
  39. package/harness/templates/task/archive-summary.md +19 -0
  40. package/harness/templates/task/events.jsonl.template +1 -0
  41. package/harness/templates/task/gate-ledger.md +21 -0
  42. package/harness/templates/task/implementation-approval.md +11 -0
  43. package/harness/templates/task/planning/00-current-planning-context.md +3 -0
  44. package/harness/templates/task/planning/01-grill-summary.md +3 -0
  45. package/harness/templates/task/planning/02-research-summary.md +3 -0
  46. package/harness/templates/task/planning/02b-compound-lookup.md +3 -0
  47. package/harness/templates/task/planning/02c-architecture-orientation.md +3 -0
  48. package/harness/templates/task/planning/03-prd.md +3 -0
  49. package/harness/templates/task/planning/04-issues.md +3 -0
  50. package/harness/templates/task/planning/05-module-structure.md +3 -0
  51. package/harness/templates/task/planning/06-writing-plan.md +3 -0
  52. package/harness/templates/task/planning/07-plan-review.md +3 -0
  53. package/harness/templates/task/planning-pack.md +23 -0
  54. package/harness/templates/task/task.yaml +10 -0
  55. package/harness/templates/task/verification.md +12 -0
  56. package/harness/verify/verify.js +271 -0
  57. package/module-template/MODULE.md +25 -0
  58. package/module-template/README.md +9 -0
  59. package/modules/.gitkeep +0 -0
  60. package/package.json +40 -0
  61. package/proposals/README.md +16 -0
  62. package/scripts/install-jjamppong-harness.ps1 +62 -0
@@ -0,0 +1,235 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const crypto = require('crypto');
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const { verifyRoot } = require('../verify/verify');
8
+
9
+ const ROOT_ITEMS = [
10
+ 'AGENTS.md',
11
+ 'README.md',
12
+ 'CONTEXT.md',
13
+ 'handoff.md',
14
+ 'harness',
15
+ 'modules',
16
+ 'module-template',
17
+ 'proposals',
18
+ ];
19
+
20
+ const EXCLUDED_DIRS = new Set([
21
+ '.git',
22
+ '.worktrees',
23
+ 'node_modules',
24
+ 'tests',
25
+ 'source-history',
26
+ ]);
27
+
28
+ function normalizeTargetPath(target) {
29
+ let text = String(target || '').trim();
30
+ if (/^[A-Za-z]:[^\\/]/.test(text)) {
31
+ text = `${text.slice(0, 2)}${path.sep}${text.slice(2)}`;
32
+ }
33
+ return path.resolve(text);
34
+ }
35
+
36
+ function sha256(filePath) {
37
+ return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
38
+ }
39
+
40
+ function ensureDir(dirPath) {
41
+ fs.mkdirSync(dirPath, { recursive: true });
42
+ }
43
+
44
+ function shouldSkip(relativePath) {
45
+ const parts = relativePath.split(/[\\/]+/);
46
+ if (parts.some((part) => EXCLUDED_DIRS.has(part))) return true;
47
+ const normalized = relativePath.replace(/\\/g, '/');
48
+ if (normalized.startsWith('harness/docs/tasks/active/')) return true;
49
+ if (normalized.startsWith('harness/docs/tasks/archive/')) return true;
50
+ if (normalized.startsWith('harness/artifacts/local/')) return true;
51
+ return false;
52
+ }
53
+
54
+ function collectFiles(root, relative = '') {
55
+ const current = path.join(root, relative);
56
+ if (!fs.existsSync(current)) return [];
57
+ const stat = fs.statSync(current);
58
+ if (stat.isFile()) return [relative];
59
+ if (!stat.isDirectory()) return [];
60
+
61
+ const files = [];
62
+ for (const entry of fs.readdirSync(current)) {
63
+ const childRelative = relative ? path.join(relative, entry) : entry;
64
+ if (shouldSkip(childRelative)) continue;
65
+ files.push(...collectFiles(root, childRelative));
66
+ }
67
+ return files;
68
+ }
69
+
70
+ function backupExisting(targetRoot, relativePath, backupRoot, rollbackFiles) {
71
+ const targetPath = path.join(targetRoot, relativePath);
72
+ if (!fs.existsSync(targetPath)) return null;
73
+
74
+ const backupPath = path.join(backupRoot, relativePath);
75
+ ensureDir(path.dirname(backupPath));
76
+ fs.copyFileSync(targetPath, backupPath);
77
+ rollbackFiles.push({
78
+ original: relativePath.replace(/\\/g, '/'),
79
+ backup: path.relative(targetRoot, backupPath).replace(/\\/g, '/'),
80
+ restored: false,
81
+ });
82
+ return backupPath;
83
+ }
84
+
85
+ function copyManagedFile(templateRoot, targetRoot, relativePath, backupRoot, rollbackFiles, managedFiles) {
86
+ const sourcePath = path.join(templateRoot, relativePath);
87
+ const targetPath = path.join(targetRoot, relativePath);
88
+ ensureDir(path.dirname(targetPath));
89
+
90
+ const sourceHash = sha256(sourcePath);
91
+ if (fs.existsSync(targetPath)) {
92
+ const targetHash = sha256(targetPath);
93
+ if (targetHash === sourceHash) {
94
+ managedFiles.push({
95
+ path: relativePath.replace(/\\/g, '/'),
96
+ sha256: sourceHash,
97
+ owner: 'harness-core',
98
+ action: 'unchanged',
99
+ });
100
+ return;
101
+ }
102
+ backupExisting(targetRoot, relativePath, backupRoot, rollbackFiles);
103
+ }
104
+
105
+ fs.copyFileSync(sourcePath, targetPath);
106
+ managedFiles.push({
107
+ path: relativePath.replace(/\\/g, '/'),
108
+ sha256: sourceHash,
109
+ owner: 'harness-core',
110
+ action: 'written',
111
+ });
112
+ }
113
+
114
+ function writeNeutralTaskDirs(targetRoot) {
115
+ for (const relative of [
116
+ 'harness/docs/tasks/active',
117
+ 'harness/docs/tasks/archive',
118
+ 'harness/artifacts/local',
119
+ ]) {
120
+ const dir = path.join(targetRoot, relative);
121
+ ensureDir(dir);
122
+ const keep = path.join(dir, '.gitkeep');
123
+ if (!fs.existsSync(keep)) fs.writeFileSync(keep, '', 'utf8');
124
+ }
125
+ }
126
+
127
+ function writeHarnessLock(targetRoot, options, managedFiles, rollbackFiles) {
128
+ const lockPath = path.join(targetRoot, 'harness.lock.yaml');
129
+ const lines = [
130
+ 'harness:',
131
+ ' name: jjamppong-harness',
132
+ ` version: ${options.version}`,
133
+ 'installer:',
134
+ ' package: "@donghyeonlee/jjamppong-harness"',
135
+ ` version: ${options.version}`,
136
+ `installed_at: "${new Date().toISOString()}"`,
137
+ `installed_from: "${options.templateRoot.replace(/\\/g, '/')}"`,
138
+ 'planning_started: false',
139
+ 'github_repo_created: false',
140
+ 'commit_created: false',
141
+ 'push_performed: false',
142
+ 'managed_files:',
143
+ ];
144
+ for (const file of managedFiles) {
145
+ lines.push(` - path: ${file.path}`);
146
+ lines.push(` sha256: ${file.sha256}`);
147
+ lines.push(` owner: ${file.owner}`);
148
+ lines.push(` action: ${file.action}`);
149
+ }
150
+ if (managedFiles.length === 0) lines.push(' []');
151
+ if (rollbackFiles.length > 0) {
152
+ lines.push('rollback_manifest:');
153
+ lines.push(` path: ${options.rollbackManifest.replace(/\\/g, '/')}`);
154
+ }
155
+ fs.writeFileSync(lockPath, `${lines.join('\n')}\n`, 'utf8');
156
+ }
157
+
158
+ function writeRollbackManifest(targetRoot, rollbackRoot, rollbackFiles) {
159
+ if (rollbackFiles.length === 0) return null;
160
+ const manifestPath = path.join(rollbackRoot, 'rollback-manifest.yaml');
161
+ const lines = [
162
+ `created_at: "${new Date().toISOString()}"`,
163
+ 'operation: install',
164
+ 'files:',
165
+ ];
166
+ for (const file of rollbackFiles) {
167
+ lines.push(` - original: ${file.original}`);
168
+ lines.push(` backup: ${file.backup}`);
169
+ lines.push(` restored: ${file.restored}`);
170
+ }
171
+ fs.writeFileSync(manifestPath, `${lines.join('\n')}\n`, 'utf8');
172
+ return path.relative(targetRoot, manifestPath);
173
+ }
174
+
175
+ function installHarness(options) {
176
+ const templateRoot = path.resolve(options.templateRoot || path.resolve(__dirname, '..', '..'));
177
+ const targetRoot = normalizeTargetPath(options.target);
178
+ const version = options.version || '0.1.0';
179
+
180
+ if (!fs.existsSync(templateRoot)) throw new Error(`Template root not found: ${templateRoot}`);
181
+ ensureDir(targetRoot);
182
+
183
+ for (const nested of ['jjamppong-harness', 'ourosuper-harness']) {
184
+ if (fs.existsSync(path.join(targetRoot, nested, 'AGENTS.md'))) {
185
+ throw new Error(`Refusing to install with forbidden nested harness folder present: ${nested}/`);
186
+ }
187
+ }
188
+
189
+ const stamp = new Date().toISOString().replace(/[-:]/g, '').replace(/\..+$/, 'Z');
190
+ const backupRoot = path.join(targetRoot, '.harness-backups', stamp);
191
+ const rollbackFiles = [];
192
+ const managedFiles = [];
193
+
194
+ for (const rootItem of ROOT_ITEMS) {
195
+ const sourcePath = path.join(templateRoot, rootItem);
196
+ if (!fs.existsSync(sourcePath)) continue;
197
+ const files = collectFiles(templateRoot, rootItem);
198
+ for (const relativePath of files) {
199
+ copyManagedFile(templateRoot, targetRoot, relativePath, backupRoot, rollbackFiles, managedFiles);
200
+ }
201
+ }
202
+
203
+ writeNeutralTaskDirs(targetRoot);
204
+ const rollbackManifest = writeRollbackManifest(targetRoot, backupRoot, rollbackFiles);
205
+ writeHarnessLock(targetRoot, { version, templateRoot, rollbackManifest }, managedFiles, rollbackFiles);
206
+
207
+ const verification = verifyRoot(targetRoot);
208
+ if (!verification.ok) {
209
+ return {
210
+ ok: false,
211
+ target: targetRoot,
212
+ installed: managedFiles.length,
213
+ rollback_manifest: rollbackManifest,
214
+ verification,
215
+ };
216
+ }
217
+
218
+ return {
219
+ ok: true,
220
+ target: targetRoot,
221
+ installed: managedFiles.length,
222
+ rollback_manifest: rollbackManifest,
223
+ verification,
224
+ stopped_after_install: true,
225
+ planning_started: false,
226
+ github_repo_created: false,
227
+ commit_created: false,
228
+ push_performed: false,
229
+ };
230
+ }
231
+
232
+ module.exports = {
233
+ installHarness,
234
+ normalizeTargetPath,
235
+ };
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ function parseArgs(argv) {
8
+ const positional = [];
9
+ const flags = {};
10
+ for (let i = 0; i < argv.length; i += 1) {
11
+ const token = argv[i];
12
+ if (!token.startsWith('--')) {
13
+ positional.push(token);
14
+ continue;
15
+ }
16
+ const key = token.slice(2);
17
+ const next = argv[i + 1];
18
+ if (!next || next.startsWith('--')) {
19
+ flags[key] = true;
20
+ } else {
21
+ flags[key] = next;
22
+ i += 1;
23
+ }
24
+ }
25
+ return { positional, flags };
26
+ }
27
+
28
+ function ensureDir(dirPath) {
29
+ fs.mkdirSync(dirPath, { recursive: true });
30
+ }
31
+
32
+ function copyTemplateDir(sourceRoot, targetRoot, replacements) {
33
+ for (const entry of fs.readdirSync(sourceRoot, { withFileTypes: true })) {
34
+ const source = path.join(sourceRoot, entry.name);
35
+ const target = path.join(targetRoot, entry.name);
36
+ if (entry.isDirectory()) {
37
+ ensureDir(target);
38
+ copyTemplateDir(source, target, replacements);
39
+ } else if (entry.isFile()) {
40
+ let text = fs.readFileSync(source, 'utf8');
41
+ for (const [key, value] of Object.entries(replacements)) {
42
+ text = text.split(`{{${key}}}`).join(value);
43
+ }
44
+ if (entry.name === 'events.jsonl.template') {
45
+ fs.writeFileSync(path.join(targetRoot, 'events.jsonl'), '', 'utf8');
46
+ } else {
47
+ fs.writeFileSync(target, text, 'utf8');
48
+ }
49
+ }
50
+ }
51
+ }
52
+
53
+ function activeTaskDirs(root) {
54
+ const activeRoot = path.join(root, 'harness', 'docs', 'tasks', 'active');
55
+ if (!fs.existsSync(activeRoot)) return [];
56
+ return fs.readdirSync(activeRoot, { withFileTypes: true })
57
+ .filter((entry) => entry.isDirectory())
58
+ .map((entry) => entry.name);
59
+ }
60
+
61
+ function createTaskSkeleton(options) {
62
+ const root = path.resolve(options.root || process.cwd());
63
+ const slug = options.slug;
64
+ const taskType = options.taskType || 'product_feature';
65
+ if (!slug || !/^[a-z0-9][a-z0-9-]*$/.test(slug)) {
66
+ throw new Error('Task slug must be lowercase ASCII kebab-case.');
67
+ }
68
+
69
+ const active = activeTaskDirs(root);
70
+ if (active.length > 0 && !active.includes(slug)) {
71
+ throw new Error(`Active task default is one. Existing active task(s): ${active.join(', ')}`);
72
+ }
73
+
74
+ const templateRoot = path.join(root, 'harness', 'templates', 'task');
75
+ const taskRoot = path.join(root, 'harness', 'docs', 'tasks', 'active', slug);
76
+ ensureDir(taskRoot);
77
+ const now = new Date().toISOString();
78
+ copyTemplateDir(templateRoot, taskRoot, {
79
+ task_id: slug,
80
+ task_type: taskType,
81
+ created_at: now,
82
+ updated_at: now,
83
+ });
84
+ return { ok: true, task: slug, task_root: taskRoot };
85
+ }
86
+
87
+ function archiveTask(options) {
88
+ const root = path.resolve(options.root || process.cwd());
89
+ const slug = options.slug;
90
+ if (!slug) throw new Error('archive-task requires --slug.');
91
+
92
+ const taskRoot = path.join(root, 'harness', 'docs', 'tasks', 'active', slug);
93
+ if (!fs.existsSync(taskRoot)) throw new Error(`Active task not found: ${slug}`);
94
+ const summary = path.join(taskRoot, 'archive-summary.md');
95
+ if (!fs.existsSync(summary)) throw new Error('archive-summary.md is required before archive.');
96
+
97
+ const now = new Date();
98
+ const yyyy = String(now.getFullYear());
99
+ const mm = String(now.getMonth() + 1).padStart(2, '0');
100
+ const archiveRoot = path.join(root, 'harness', 'docs', 'tasks', 'archive', yyyy, mm, slug);
101
+ ensureDir(path.dirname(archiveRoot));
102
+ if (fs.existsSync(archiveRoot)) throw new Error(`Archive task already exists: ${archiveRoot}`);
103
+ fs.renameSync(taskRoot, archiveRoot);
104
+ return { ok: true, task: slug, archive_root: archiveRoot };
105
+ }
106
+
107
+ function main() {
108
+ const { positional, flags } = parseArgs(process.argv.slice(2));
109
+ const command = positional[0];
110
+ let result;
111
+ if (command === 'create-task') {
112
+ result = createTaskSkeleton({ root: flags.root, slug: flags.slug, taskType: flags['task-type'] });
113
+ } else if (command === 'archive-task') {
114
+ result = archiveTask({ root: flags.root, slug: flags.slug });
115
+ } else {
116
+ throw new Error('Usage: lifecycle.js create-task|archive-task --root <root> --slug <slug>');
117
+ }
118
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
119
+ }
120
+
121
+ if (require.main === module) {
122
+ try {
123
+ main();
124
+ } catch (error) {
125
+ process.stderr.write(`ERROR ${error.message}\n`);
126
+ process.exitCode = 1;
127
+ }
128
+ }
129
+
130
+ module.exports = {
131
+ createTaskSkeleton,
132
+ archiveTask,
133
+ };