@manhd_d/ai-harness 0.13.0 → 0.14.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/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased — Guided start and portable configuration
4
+
5
+ - Add start: short terminal intake, saved task/source/output choices, read-only non-TTY plan and host handoff.
6
+ - Add bounded configuration backup/restore, dry-run conflicts, explicit replacement and before-backup; transfer only module/model preferences with old repo autonomy removed.
7
+ - Keep canonical user config paths, ignore local backups, preserve settings through update, and document config vs full-project recovery in all README entry points.
8
+ - Validate archive/config/path safety before writes; add rollback/transfer/intake tests, package coverage and unscored host scenarios. No model/network calls or distribution bump.
9
+
3
10
  ## Unreleased — Kiro and clearer tool setup
4
11
 
5
12
  - Add Kiro to all README languages and a documented IDE/CLI/Web setup guide with optional native skill pointers.
@@ -139,6 +146,13 @@
139
146
  - Check release metadata consistency and establish single-release-PR version discipline; retain v0.6.0 until a dedicated release PR.
140
147
 
141
148
 
149
+ ## [0.14.0](https://github.com/manh0303/ai-harness/compare/v0.13.0...v0.14.0) (2026-10-14)
150
+
151
+
152
+ ### Features
153
+
154
+ * add guided start and portable user configuration ([2f18d23](https://github.com/manh0303/ai-harness/commit/2f18d23d92811858a3b722b349f423694bfcf3e2))
155
+
142
156
  ## [0.13.0](https://github.com/manh0303/ai-harness/compare/v0.12.0...v0.13.0) (2026-10-13)
143
157
 
144
158
 
package/README.md CHANGED
@@ -14,8 +14,11 @@ Install/sign in to an AI tool below. Requires Node.js 22+. Run in your **project
14
14
 
15
15
  ```bash
16
16
  npx --yes @manhd_d/ai-harness init
17
+ npx --yes @manhd_d/ai-harness start
17
18
  ```
18
19
 
20
+ Run `start` for guided intake, then send its `host_prompt` to your AI tool. [Settings, backup & transfer](docs/configuration-backup.md).
21
+
19
22
  `npx` does not install a global `aih` command. Check the installation with
20
23
  `npx --yes @manhd_d/ai-harness check`; then open the same project in your AI tool.
21
24
 
package/bin/aih.js CHANGED
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env node
2
2
  import { main } from '../cli/main.js';
3
3
 
4
- process.exitCode = main();
4
+ process.exitCode = await main();
package/cli/harness.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import fs from 'node:fs';
2
+ import { loadStartConfig } from './start-config.js';
2
3
  import { checkpointPath, validateCheckpoint } from './checkpoint.js';
3
4
  import { readConfig } from './project-config.js';
4
5
  import { claudeReport, claudeBlock, linkClaude } from './claude.js';
@@ -14,7 +15,7 @@ import { resolveModules } from './composition.js';
14
15
  import { safe, readJson, manifest, encode, digest, stat, block, mergeAgents, payload, validateAdapters, validateCapabilities, assertSeparate, HarnessError, LOCK, CONFIG, VERSION, SLUG, isObject } from './files.js';
15
16
  export * from './files.js';
16
17
 
17
- function projectConfig(root, adapters, capabilities, required = false) {
18
+ export function projectConfig(root, adapters, capabilities, required = false) {
18
19
  const filename = safe(root, CONFIG);
19
20
  if (required && !stat(filename)) throw new HarnessError(`Missing ${CONFIG}; restore it from Git`);
20
21
  const config = stat(filename) ? readJson(filename) : { schema_version: 1, adapters: adapters ?? [], capabilities: capabilities ?? [] };
@@ -120,7 +121,7 @@ function drift(root, lock) {
120
121
  }).map(([name]) => name);
121
122
  }
122
123
 
123
- function guard(root, operation) {
124
+ export function guard(root, operation) {
124
125
  const filename = safe(root, '.harness-install.lock');
125
126
  let fd;
126
127
  try { fd = fs.openSync(filename, 'wx', 0o600); }
@@ -132,12 +133,12 @@ function guard(root, operation) {
132
133
  finally { fs.closeSync(fd); fs.unlinkSync(filename); }
133
134
  }
134
135
 
135
- export function atomicWrite(filename, data) {
136
+ export function atomicWrite(filename, data, mode = 0o644) {
136
137
  fs.mkdirSync(path.dirname(filename), { recursive: true });
137
138
  const tmp = path.join(path.dirname(filename), `.harness-tmp-${randomUUID()}`);
138
139
  try {
139
140
  fs.writeFileSync(tmp, data, { flag: 'wx', mode: 0o600 });
140
- fs.chmodSync(tmp, stat(filename) ? fs.statSync(filename).mode & 0o777 : 0o644);
141
+ fs.chmodSync(tmp, stat(filename) ? fs.statSync(filename).mode & 0o777 : mode);
141
142
  fs.renameSync(tmp, filename);
142
143
  } finally { fs.rmSync(tmp, { force: true }); }
143
144
  }
@@ -172,6 +173,7 @@ export function install(root, source, { command = 'init', adapters, capabilities
172
173
  if (command === 'update' && !hasLock) throw new HarnessError('Not installed; run init first');
173
174
  const old = hasLock ? loadLock(root) : null;
174
175
  const existingAgents = loadAgentConfig(root);
176
+ loadStartConfig(root);
175
177
  loadArtifactContract(root); // Validate project-owned declarations before planning any writes.
176
178
  if (agentPreset !== undefined && command !== 'init') throw new HarnessError('Agent preset is supported only by init');
177
179
  if (agentPreset !== undefined && existingAgents) throw new HarnessError('agents.json is project-owned; edit it explicitly instead of applying an init preset');
@@ -230,8 +232,8 @@ export function install(root, source, { command = 'init', adapters, capabilities
230
232
  // This does not remove an already tracked machine file from the Git index.
231
233
  const ignoreName = '.harness/.gitignore', ignorePath = safe(root, ignoreName);
232
234
  const ignore = stat(ignorePath) ? fs.readFileSync(ignorePath, 'utf8') : '';
233
- if (ignore.trimEnd().split(/\r?\n/).at(-1) !== '/machine.json') {
234
- changes[ignoreName] = Buffer.from(ignore + (ignore && !ignore.endsWith('\n') ? '\n' : '') + '/machine.json\n');
235
+ if (ignore.trimEnd().split(/\r?\n/).slice(-2).join('\n') !== '/backups/\n/machine.json') {
236
+ changes[ignoreName] = Buffer.from(ignore + (ignore && !ignore.endsWith('\n') ? '\n' : '') + '/backups/\n/machine.json\n');
235
237
  }
236
238
  const newLock = encode(lock);
237
239
  if (!hasLock || !fs.readFileSync(safe(root, LOCK)).equals(newLock)) changes[LOCK] = newLock;
@@ -245,6 +247,7 @@ export function check(root) {
245
247
  const lock = loadLock(root), config = projectConfig(root, undefined, undefined, true);
246
248
  const problems = drift(root, lock);
247
249
  loadArtifactContract(root);
250
+ loadStartConfig(root);
248
251
  loadAgentConfig(root); // Optional on legacy installs; validate user config without managing its bytes.
249
252
  if (JSON.stringify([...config.adapters].sort()) !== JSON.stringify([...lock.adapters].sort())) problems.push(`${CONFIG}: adapter selection changed; run update`);
250
253
  if (JSON.stringify([...(config.capabilities ?? [])].sort()) !== JSON.stringify([...(lock.capabilities ?? [])].sort())) problems.push(`${CONFIG}: capability selection changed; run update`);
package/cli/main.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { startPlan, saveStart, guidedStart } from './start.js';
2
+ import { backupConfig, restoreConfig } from './portable-config.js';
1
3
  import { listAdapters, detect, verify, evaluate } from './adapters.js';
2
4
  import { listCapabilities, skillCatalog, readSkill } from './capabilities.js';
3
5
  import { resolveModules } from './composition.js';
@@ -29,6 +31,9 @@ Usage: aih <command> [options]
29
31
  update Apply the installed CLI bundle to the project (no download)
30
32
  check Check Harness file integrity, not application tests
31
33
  doctor Check installation, configured paths and tool presence
34
+ start Guide intake; reuse saved answers and hand off to your AI host
35
+ backup Export user configuration (not docs/checkpoints/secrets)
36
+ restore Restore --from backup; --transfer reuses modules/model preferences
32
37
  setup Suggest first-run questions for the AI host (read-only)
33
38
  artifacts Show input/output/storage and connection declarations (read-only)
34
39
  ready <id> Check structural change readiness, not approval
@@ -36,6 +41,13 @@ Usage: aih <command> [options]
36
41
  new-doc <type> Scaffold a locked capability template (default: product)
37
42
 
38
43
  Options:
44
+ --goal <type> start: idea, docs, change, resume (saves with --task)
45
+ --task <text> start: task description; --change supplies resume checkpoint ID
46
+ --input <path/url> start: seed requirements only if no inputs declared
47
+ --output <path> start: draft docs directory; backup: archive path
48
+ --from <file> restore: project-relative archive file
49
+ --transfer restore: only modules/agent models; omit old autonomy
50
+ --replace restore: explicit replacement; automatic before-backup
39
51
  --target <directory> Project directory (default: current directory)
40
52
  --detect init only: opt in to detected stack selection
41
53
  --stage <type> ready only: implement (default) or review
@@ -56,7 +68,9 @@ Options:
56
68
  -h, --help Show help
57
69
  -v, --version Show installed CLI version
58
70
 
59
- Start: init → fill .harness/local.md → ask your AI host to read AGENTS.md.
71
+ Start: init → start → open your AI host and send the returned host_prompt.
72
+ User settings: .harness/; backup exports only documented user files.
73
+ Restore config before init on a fresh project; --transfer excludes old sources/tasks.
60
74
  The host runs application tests; verify prints plans or validates supplied evidence.
61
75
  Update a global CLI with npm install -g @manhd_d/ai-harness@latest,
62
76
  then run aih update --dry-run, aih update, and aih check in the project.
@@ -95,7 +109,7 @@ export function main(argv = process.argv.slice(2)) {
95
109
  let args;
96
110
  try {
97
111
  args = parseArgs({ args: argv, allowPositionals: true, strict: true, tokens: true, options: {
98
- target: { type: 'string' }, adapters: { type: 'string' }, capabilities: { type: 'string' }, capability: { type: 'string' }, source: { type: 'string' },
112
+ goal: { type: 'string' }, task: { type: 'string' }, change: { type: 'string' }, input: { type: 'string' }, output: { type: 'string' }, from: { type: 'string' }, transfer: { type: 'boolean' }, replace: { type: 'boolean' }, target: { type: 'string' }, adapters: { type: 'string' }, capabilities: { type: 'string' }, capability: { type: 'string' }, source: { type: 'string' },
99
113
  'agent-preset': { type: 'string' }, host: { type: 'string' }, expected: { type: 'string' },
100
114
  'link-claude': { type: 'boolean' }, 'claude-skills': { type: 'boolean' }, adaptive: { type: 'boolean' }, detect: { type: 'boolean' }, report: { type: 'string' }, scope: { type: 'string' }, stage: { type: 'string' },
101
115
  'dry-run': { type: 'boolean' }, 'allow-major': { type: 'boolean' },
@@ -109,9 +123,13 @@ export function main(argv = process.argv.slice(2)) {
109
123
  if (args.values.help || !argv.length) { console.log(HELP); return 0; }
110
124
  if (args.values.version) { console.log(readJson(path.join(SOURCE, 'package.json')).version); return 0; }
111
125
  const [command, name, ...extra] = args.positionals;
112
- if (!['init', 'update', 'check', 'doctor', 'new-change', 'new-doc', 'list', 'detect', 'resolve', 'verify', 'eval', 'skills', 'skill', 'setup', 'ready', 'artifacts', 'agents', 'checkpoint', 'resume'].includes(command)) throw new Error(`Unknown command: ${command}`);
126
+ if (!['start', 'backup', 'restore', 'init', 'update', 'check', 'doctor', 'new-change', 'new-doc', 'list', 'detect', 'resolve', 'verify', 'eval', 'skills', 'skill', 'setup', 'ready', 'artifacts', 'agents', 'checkpoint', 'resume'].includes(command)) throw new Error(`Unknown command: ${command}`);
113
127
  if (extra.length || (!['new-change', 'new-doc', 'skill', 'ready', 'checkpoint', 'resume'].includes(command) && name !== undefined) || (['new-change', 'new-doc', 'skill', 'ready', 'checkpoint', 'resume'].includes(command) && name === undefined)) throw new Error('Unexpected or missing positional argument; run aih --help');
114
128
  const allowed = new Set(['target', ...(['init', 'update'].includes(command) ? ['source', 'dry-run', 'allow-major'] : []), ...(['init', 'resolve'].includes(command) ? ['adapters', 'capabilities'] : []), ...(command === 'init' ? ['detect'] : []), ...(['list', 'detect', 'resolve'].includes(command) ? ['source'] : []), ...(command === 'eval' ? ['report'] : []), ...(command === 'ready' ? ['stage'] : []), ...(['verify', 'eval'].includes(command) ? ['scope'] : [])]);
129
+ if (command === 'start') for (const key of ['goal','task','change','input','output','host']) allowed.add(key);
130
+ if (command === 'backup') allowed.add('output');
131
+ if (command === 'restore') { for (const key of ['from','transfer','replace','dry-run']) allowed.add(key); if (!args.values.from) throw new Error('restore requires --from'); }
132
+ if (command === 'start' && !args.values.goal && ['task','change','input','output'].some(key => args.values[key] !== undefined)) throw new Error('start saving options require --goal');
115
133
  if (command === 'checkpoint') { allowed.add('report'); allowed.add('expected'); if (!args.values.report) throw new Error('checkpoint requires --report'); }
116
134
  if (command === 'new-doc') { allowed.add('capability'); allowed.add('report'); }
117
135
  if (command === 'verify') { allowed.add('adaptive'); allowed.add('report'); }
@@ -122,13 +140,18 @@ export function main(argv = process.argv.slice(2)) {
122
140
  if (['init','update'].includes(command)) { allowed.add('link-claude'); allowed.add('claude-skills'); }
123
141
  for (const key of Object.keys(args.values)) if (!allowed.has(key)) throw new Error(`--${key} is not supported for ${command}`);
124
142
  if (args.values.detect && args.values.adapters !== undefined) throw new Error('Choose either --detect or --adapters');
125
- for (const key of ['target', 'source', 'adapters', 'capabilities', 'capability', 'report', 'agent-preset', 'host']) if (args.values[key] !== undefined && !args.values[key].trim()) throw new Error(`--${key} cannot be empty`);
143
+ for (const key of ['goal', 'task', 'change', 'input', 'output', 'from', 'target', 'source', 'adapters', 'capabilities', 'capability', 'report', 'agent-preset', 'host']) if (args.values[key] !== undefined && !args.values[key].trim()) throw new Error(`--${key} cannot be empty`);
126
144
  } catch (error) { console.error(JSON.stringify({ status: 'error', message: error.message })); return 2; }
127
145
  try {
128
146
  const { values, positionals: [command, name] } = args;
129
147
  const root = targetRoot(values.target ?? process.cwd());
130
148
  let result;
131
- if (command === 'init' || command === 'update') {
149
+ if (command === 'start') {
150
+ if (!values.goal && process.stdin.isTTY && process.stdout.isTTY) return guidedStart(root, { host: values.host }).then(value => { console.log(JSON.stringify(value, null, 2)); return value.status === 'blocked' ? 1 : 0; }).catch(error => { console.error(JSON.stringify({ status: 'error', message: error.message })); return 1; });
151
+ result = values.goal ? saveStart(root, { goal: values.goal, task: values.task, changeId: values.change, input: values.input, output: values.output, host: values.host }) : startPlan(root, values.host);
152
+ } else if (command === 'backup') result = backupConfig(root, values.output);
153
+ else if (command === 'restore') result = restoreConfig(root, values.from, { dryRun: values['dry-run'], transfer: values.transfer, replace: values.replace });
154
+ else if (command === 'init' || command === 'update') {
132
155
  const source = targetRoot(values.source ?? SOURCE);
133
156
  const selection = values.detect ? detect(root, source).adapters : undefined;
134
157
  if (values.detect && !selection.length) throw new Error('No stack detected; choose --adapters explicitly or omit --detect for core only');
@@ -172,9 +195,9 @@ export function main(argv = process.argv.slice(2)) {
172
195
  else result = command === 'doctor' ? doctor(root) : check(root);
173
196
  if (command === 'init' || command === 'update') result.next_steps = values['dry-run']
174
197
  ? ['Review the proposed changes, then repeat without --dry-run.']
175
- : ['Run aih setup; ask your AI host to read AGENTS.md and the project-setup workflow.',
198
+ : ['Run start with the same CLI (aih start or npx --yes @manhd_d/ai-harness start); open your AI host and send host_prompt.',
176
199
  'Review local.md/environment.json and artifacts.json/integrations.json; record input/output/storage/ownership, keep credentials in the host and personal paths in ignored machine.json.',
177
- 'Run aih doctor and review/commit the shared configuration and snapshot.'];
200
+ 'Run doctor and review/commit shared configuration and snapshot. Use backup; copy its file outside the repo. See docs/configuration-backup.md.'];
178
201
  if (command === 'new-change') result.next_steps = [
179
202
  'Resolve consequential ambiguity with the user and record answers/sources in decisions.json.',
180
203
  'When artifact outputs are configured, record create/update/unchanged impact and source-write policy in artifact-impact.json.',
@@ -0,0 +1,121 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ import { randomUUID } from 'node:crypto';
5
+ import { safe, stat, encode, digest, isObject, SOURCE, VERSION, HarnessError } from './files.js';
6
+ import { projectConfig, guard, transaction, atomicWrite } from './harness.js';
7
+ import { loadEnvironment } from './environment.js';
8
+ import { loadAgentConfig } from './agents.js';
9
+ import { loadArtifactContract } from './artifacts.js';
10
+ import { loadStartConfig } from './start-config.js';
11
+ import { resolveModules } from './composition.js';
12
+
13
+ export const USER_FILES = ['.harness/project.json', '.harness/agents.json', '.harness/artifacts.json',
14
+ '.harness/integrations.json', '.harness/environment.json', '.harness/local.md', '.harness/start.json', 'openspec/config.yaml'];
15
+ const MAX_FILE = 1024 * 1024, MAX_ARCHIVE = 12 * MAX_FILE;
16
+ const fail = (ok, message) => { if (!ok) throw new HarnessError(message); };
17
+ export const backupName = () => '.harness/backups/config-' + new Date().toISOString().replace(/[:.]/g, '-') + '-' + randomUUID() + '.json';
18
+ function readText(root, name) {
19
+ const filename = safe(root, name), info = stat(filename);
20
+ if (!info) return null;
21
+ fail(info.size <= MAX_FILE, 'Configuration exceeds 1 MiB: ' + name);
22
+ const bytes = fs.readFileSync(filename), content = bytes.toString('utf8');
23
+ fail(Buffer.from(content).equals(bytes), 'Configuration must be UTF-8: ' + name);
24
+ return content;
25
+ }
26
+ export function configArchive(root) {
27
+ const files = {};
28
+ for (const name of USER_FILES) {
29
+ const content = readText(root, name);
30
+ if (content !== null) files[name] = { content, sha256: digest(Buffer.from(content)) };
31
+ }
32
+ fail(Object.hasOwn(files, '.harness/project.json'), 'Missing .harness/project.json; initialize or restore project configuration first');
33
+ return { schema_version: 1, kind: 'ai-harness-user-config', cli_version: JSON.parse(fs.readFileSync(path.join(SOURCE, 'package.json'))).version,
34
+ files };
35
+ }
36
+ function validateArchive(value) {
37
+ fail(isObject(value) && Object.keys(value).sort().join(',') === 'cli_version,files,kind,schema_version'
38
+ && value.schema_version === 1 && value.kind === 'ai-harness-user-config' && VERSION.test(value.cli_version)
39
+ && isObject(value.files) && Object.hasOwn(value.files, '.harness/project.json'), 'Unsupported configuration backup schema');
40
+ for (const [name, file] of Object.entries(value.files)) {
41
+ fail(USER_FILES.includes(name), 'Backup contains an unapproved configuration path');
42
+ fail(isObject(file) && Object.keys(file).sort().join(',') === 'content,sha256' && typeof file.content === 'string'
43
+ && Buffer.byteLength(file.content) <= MAX_FILE && /^[a-f0-9]{64}$/.test(file.sha256)
44
+ && digest(Buffer.from(file.content)) === file.sha256, 'Backup content/digest mismatch: ' + name);
45
+ }
46
+ }
47
+ function validateFiles(files) {
48
+ const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'aih-config-'));
49
+ try {
50
+ for (const [name, file] of Object.entries(files)) {
51
+ if (name.endsWith('.json')) { try { JSON.parse(file.content); } catch { throw new HarnessError('Invalid JSON configuration: ' + name); } }
52
+ const dest = safe(temp, name); fs.mkdirSync(path.dirname(dest), { recursive: true }); fs.writeFileSync(dest, file.content, { mode: 0o600 });
53
+ }
54
+ const config = projectConfig(temp, undefined, undefined, true);
55
+ resolveModules(SOURCE, config);
56
+ loadAgentConfig(temp); loadArtifactContract(temp); loadEnvironment(temp); loadStartConfig(temp);
57
+ } finally { fs.rmSync(temp, { recursive: true, force: true }); }
58
+ }
59
+ function outputPath(root, name) {
60
+ fail(typeof name === 'string' && name.endsWith('.json') && !/^(?:\.git|\.agents|\.claude|node_modules)(?:\/|$)/.test(name) && !USER_FILES.includes(name) && !name.startsWith('.harness/core/') && !name.startsWith('openspec/changes/')
61
+ && (!name.startsWith('.harness/') || name.startsWith('.harness/backups/')), 'Use a backup path outside active Harness configuration');
62
+ const filename = safe(root, name); fail(!stat(filename), 'Backup destination already exists'); return filename;
63
+ }
64
+ export function backupConfig(root, output = backupName()) {
65
+ return guard(root, () => {
66
+ const archive = configArchive(root); validateArchive(archive); validateFiles(archive.files);
67
+ const filename = outputPath(root, output);
68
+ fs.mkdirSync(path.dirname(filename), { recursive: true });
69
+ fs.writeFileSync(filename, encode(archive), { flag: 'wx', mode: 0o600 });
70
+ return { status: 'ok', backup: output, included: Object.keys(archive.files),
71
+ excluded: ['machine.json', 'credentials and native host settings', 'managed snapshot and lock', 'custom skills', 'source/output document contents', 'OpenSpec changes, checkpoints and evidence'],
72
+ next_steps: ['Copy the backup to a private location outside this project.', 'Keep code/docs/openspec changes and custom skills in your repository backup; configuration backup is not a full project backup.'] };
73
+ });
74
+ }
75
+ export function restoreConfig(root, from, { dryRun = false, transfer = false, replace = false, writer } = {}) {
76
+ const filename = safe(root, from); fail(stat(filename)?.size <= MAX_ARCHIVE, 'Missing or oversized configuration backup');
77
+ const archive = readConfigArchive(filename); validateArchive(archive); validateFiles(archive.files);
78
+ return guard(root, () => {
79
+ const files = transfer ? Object.fromEntries(Object.entries(archive.files).filter(([name]) => ['.harness/project.json', '.harness/agents.json'].includes(name))) : archive.files;
80
+ if (transfer && files['.harness/agents.json']) {
81
+ const config = JSON.parse(files['.harness/agents.json'].content);
82
+ delete config.execution.autonomy; // Repository-scoped autonomy decisions never move to a different project.
83
+ files['.harness/agents.json'] = { content: encode(config).toString() };
84
+ }
85
+ const changes = {}, conflicts = [];
86
+ for (const [name, file] of Object.entries(files)) {
87
+ const current = readText(root, name);
88
+ if (current === file.content) continue;
89
+ if (current !== null) conflicts.push(name);
90
+ changes[name] = Buffer.from(file.content);
91
+ }
92
+ fail(!conflicts.length || replace || dryRun, 'Existing configuration differs: ' + conflicts.join(', ') + '; review --dry-run --replace before replacement');
93
+ // Validate the prospective merged configuration before any target writes.
94
+ const merged = {};
95
+ for (const name of USER_FILES) {
96
+ const content = changes[name]?.toString() ?? readText(root, name);
97
+ if (content !== null) merged[name] = { content };
98
+ }
99
+ validateFiles(merged);
100
+ // Validate declared artifact paths against actual target symlinks, not only temporary files.
101
+ const artifacts = merged['.harness/artifacts.json'] ? JSON.parse(merged['.harness/artifacts.json'].content) : null;
102
+ for (const group of ['inputs','outputs']) for (const item of Object.values(artifacts?.[group] ?? {})) {
103
+ if (item.location.path) safe(root, item.location.path + (group === 'outputs' || stat(path.join(root, item.location.path))?.isDirectory() ? '/.artifact-probe' : ''));
104
+ }
105
+ let backup = null;
106
+ if (!dryRun && conflicts.length) {
107
+ const before = configArchive(root); backup = backupName(); outputPath(root, backup);
108
+ changes[backup] = encode(before);
109
+ }
110
+ if (!dryRun) transaction(root, changes, writer ?? ((filename, data) => atomicWrite(filename, data, filename === (backup ? safe(root, backup) : null) ? 0o600 : 0o644)));
111
+ return { status: dryRun ? 'planned' : 'ok', mode: transfer ? 'transfer' : 'restore', changes: Object.keys(changes), conflicts,
112
+ backup, omitted: Object.keys(archive.files).filter(name => !Object.hasOwn(files, name)),
113
+ next_steps: ['Run init on a fresh project, or update --dry-run then update on an installed project; run check.',
114
+ transfer ? 'Run start for a new task; rebind input/output locations, commands and connections. Old autonomy declarations were removed.' : 'Run doctor and start; restore docs/checkpoints/evidence separately from your project backup before resume.',
115
+ 'Review host access, models, sources and write policies; importing declarations does not grant permission or connect accounts.'] };
116
+ });
117
+ }
118
+ function readConfigArchive(filename) {
119
+ try { return JSON.parse(fs.readFileSync(filename, 'utf8')); }
120
+ catch { throw new HarnessError('Invalid configuration backup JSON'); }
121
+ }
@@ -0,0 +1,15 @@
1
+ import { readConfig } from './project-config.js';
2
+ import { isObject, SLUG, HarnessError, safe, stat } from './files.js';
3
+ export const START_CONFIG = '.harness/start.json';
4
+ export const GOALS = ['idea', 'docs', 'change', 'resume'];
5
+ export function loadStartConfig(root) {
6
+ const value = readConfig(root, START_CONFIG);
7
+ if (value === null) { if (stat(safe(root, START_CONFIG))) throw new HarnessError('Invalid start.json schema'); return null; }
8
+ const text = s => typeof s === 'string' && s.trim().length > 0 && s.length <= 4096 && !/[\x00-\x1f\x7f]/.test(s);
9
+ if (!isObject(value) || Object.keys(value).some(k => !['schema_version', 'goal', 'task', 'change_id'].includes(k))
10
+ || value.schema_version !== 1 || !GOALS.includes(value.goal) || !text(value.task)
11
+ || (value.goal === 'resume' ? !SLUG.test(value.change_id ?? '') || value.change_id === 'archive' : value.change_id !== undefined)) {
12
+ throw new HarnessError('Invalid start.json schema/goal/task/change_id');
13
+ }
14
+ return value;
15
+ }
package/cli/start.js ADDED
@@ -0,0 +1,138 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ import { createInterface } from 'node:readline/promises';
5
+ import { safe, stat, encode, SLUG, HarnessError } from './files.js';
6
+ import { check, loadLock, guard, transaction, atomicWrite } from './harness.js';
7
+ import { setupPlan } from './environment.js';
8
+ import { loadArtifactContract, emptyArtifacts } from './artifacts.js';
9
+ import { START_CONFIG, GOALS, loadStartConfig } from './start-config.js';
10
+ import { configArchive, backupName } from './portable-config.js';
11
+ import { resumePlan } from './checkpoint.js';
12
+
13
+ function assert(ok, message) { if (!ok) throw new HarnessError(message); }
14
+ export function startPlan(root, host) {
15
+ assert(check(root).status === 'ok', 'Fix installation drift before start');
16
+ const lock = loadLock(root), saved = loadStartConfig(root), contract = loadArtifactContract(root);
17
+ const evidence = ['README.md', 'docs', 'requirements', 'pom.xml', 'package.json', 'build.gradle', 'pyproject.toml']
18
+ .filter(name => { const p = path.join(root, name), info = stat(p); if (info?.isDirectory()) safe(root, name + '/.start-probe'); else safe(root, name); return Boolean(info); });
19
+ const base = path.join(root, 'openspec/changes'), checkpoints = [];
20
+ if (stat(base)) {
21
+ safe(root, 'openspec/changes/.start-probe');
22
+ for (const id of fs.readdirSync(base).sort().slice(0, 100)) {
23
+ if (id !== 'archive' && SLUG.test(id) && stat(safe(root, 'openspec/changes/' + id + '/checkpoint.json'))) checkpoints.push(id);
24
+ }
25
+ }
26
+ const recommendation = saved?.goal ?? (checkpoints.length ? 'resume' : contract.artifacts.mode === 'document-driven' ? 'docs'
27
+ : evidence.some(p => ['pom.xml', 'package.json', 'build.gradle', 'pyproject.toml'].includes(p)) ? 'change' : 'idea');
28
+ const goal = saved?.goal;
29
+ const workflow = goal === 'idea' ? 'solution-framing.md' : 'continuous-development.md';
30
+ const setup = setupPlan(root, lock.adapters, process.env, host);
31
+ let resume = null;
32
+ if (goal === 'resume') {
33
+ try { resume = resumePlan(root, saved.change_id); }
34
+ catch (error) { if (!(error instanceof HarnessError)) throw error; resume = { status: 'blocked', executed: false, message: error.message, next_action: 'Restore/reconcile checkpoints and evidence before continuation, or choose a new task.' }; }
35
+ }
36
+ return { status: resume?.status === 'blocked' ? 'blocked' : 'planned', saved, discovery: { evidence, checkpoints, inputs: Object.keys(contract.artifacts.inputs), outputs: Object.keys(contract.artifacts.outputs) },
37
+ recommendation, reason: saved ? 'Reuse the saved task; choose a new goal explicitly to start another task.'
38
+ : checkpoints.length ? 'Checkpoint candidates exist; inspect freshness before continuing.'
39
+ : 'Based on declared sources and root build-file presence; the host must inspect their contents.',
40
+ questions: [ ...(!saved ? [{ id: 'goal', question: 'Start from an idea, existing docs, change existing code, or resume?', options: GOALS, recommendation }] : []),
41
+ ...(!Object.keys(contract.artifacts.inputs).length && goal !== 'resume' ? [{ id: 'input', question: 'Use which requirement path or credential-free HTTPS URL? Leave unknowns for the AI discussion.', options: ['repository', 'external', 'idea'], recommendation: evidence.includes('docs') ? 'repository' : 'idea' }] : []),
42
+ ...(!Object.keys(contract.artifacts.outputs).length && ['idea','docs'].includes(goal) ? [{ id: 'output', question: 'Where should draft docs be written?', options: ['docs/product', 'existing destination', 'discuss with AI'], recommendation: 'docs/product' }] : []) ],
43
+ suggested_roles: goal === 'idea' ? ['product:business-analyst', 'architecture:solution-architect']
44
+ : goal === 'docs' ? ['core:planner', 'core:implementer', 'core:reviewer'] : ['core:implementer', 'core:reviewer'],
45
+ agent_routes: setup.agent_configuration.roles.filter(role => (goal === 'idea' ? ['product:business-analyst', 'architecture:solution-architect'] : goal === 'docs' ? ['core:planner', 'core:implementer', 'core:reviewer'] : ['core:implementer', 'core:reviewer']).includes(role.canonical_id)),
46
+ installed_modules: { adapters: lock.adapters, capabilities: lock.capabilities ?? [] },
47
+ setup_questions: setup.questions, environment: setup.environment,
48
+ resume,
49
+ configuration: { directory: '.harness/', task: START_CONFIG, inputs_outputs: '.harness/artifacts.json', models_agents: '.harness/agents.json',
50
+ backup: 'aih backup', restore: 'aih restore --from <project-relative-backup.json>', transfer: 'aih restore --from <backup.json> --transfer' },
51
+ host_prompt: 'Read AGENTS.md, .harness/start.json if present, .harness/local.md and .harness/core/workflows/project-setup.md. '
52
+ + 'Inspect declared input/output locations and current code. Reuse saved answers; ask only missing consequential decisions with options and a grounded recommendation. '
53
+ + 'Use .harness/core/workflows/' + workflow + '. Discover only relevant installed agents/skills; suggest missing modules before editing project.json. '
54
+ + 'Keep model defaults unless requested; save accepted source/output/connection choices in their project-owned configs. '
55
+ + 'For resumed tasks inspect checkpoint freshness and side effects. Implement accepted scope with actual verification and maintain affected docs.',
56
+ next_steps: ['In a terminal start interviews and saves answers; without a TTY it prints this plan. Use --goal/--task to save non-interactively.',
57
+ 'Open this project in your AI tool and send host_prompt; CLI does not launch a model or read remote document contents.',
58
+ 'Use backup and copy its file outside the repository; see the configuration backup guide.'] };
59
+ }
60
+ export function saveStart(root, { goal, task, changeId, input, output, host } = {}) {
61
+ assert(GOALS.includes(goal), 'Choose --goal idea|docs|change|resume');
62
+ assert(typeof task === 'string' && task.trim().length > 0 && task.length <= 4096 && !/[\x00-\x1f\x7f]/.test(task), 'Supply a nonempty single-line --task (max 4096 characters)');
63
+ assert(goal === 'resume' ? SLUG.test(changeId ?? '') && changeId !== 'archive' : changeId === undefined, 'Use --change only with resume and an existing change ID');
64
+ return guard(root, () => {
65
+ const plan = startPlan(root, host), contract = loadArtifactContract(root);
66
+ if (goal === 'resume') {
67
+ assert(plan.discovery.checkpoints.includes(changeId), 'Resume requires an existing checkpoint; restore project changes/evidence first');
68
+ resumePlan(root, changeId); // Validate checkpoint/evidence before saving any configuration.
69
+ }
70
+ assert(goal !== 'resume' || (input === undefined && output === undefined), 'Resume reuses source/output contracts; do not combine with --input/--output');
71
+ const changes = {}, value = { schema_version: 1, goal, task: task.trim(), ...(changeId ? { change_id: changeId } : {}) };
72
+ changes[START_CONFIG] = encode(value);
73
+ if (input !== undefined || output !== undefined) {
74
+ const artifacts = structuredClone(contract.artifacts);
75
+ if (!contract.configured) Object.assign(artifacts, emptyArtifacts(), { mode: input ? 'document-driven' : goal === 'docs' ? 'document-driven' : 'idea-driven' });
76
+ if (input !== undefined) {
77
+ assert(!Object.keys(artifacts.inputs).length, 'Inputs already configured; review artifacts.json instead of replacing them through start');
78
+ assert(typeof input === 'string' && input.trim().length > 0, 'Input cannot be empty');
79
+ artifacts.inputs.requirements = { kind: 'requirements', location: input.startsWith('https://') ? { url: input } : { path: input }, write_policy: 'read-only' };
80
+ }
81
+ if (output !== undefined) {
82
+ assert(!Object.keys(artifacts.outputs).length, 'Outputs already configured; review artifacts.json instead of replacing them through start');
83
+ artifacts.outputs.documents = { kind: 'docs', location: { path: output }, capability: 'product', write_policy: 'propose', sources: Object.keys(artifacts.inputs) };
84
+ }
85
+ // Reuse the existing contract validator in a temporary workspace, before touching the target.
86
+ validateProposed(root, artifacts);
87
+ changes['.harness/artifacts.json'] = encode(artifacts);
88
+ }
89
+ for (const [name, bytes] of Object.entries(changes)) if (stat(safe(root, name)) && fs.readFileSync(safe(root, name)).equals(bytes)) delete changes[name];
90
+ const replacing = Object.keys(changes).some(name => stat(safe(root, name)));
91
+ let backup = null;
92
+ if (replacing) { backup = backupName(); assert(!stat(safe(root, backup)), 'Backup collision'); changes[backup] = encode(configArchive(root)); }
93
+ transaction(root, changes, (filename, data) => atomicWrite(filename, data, filename === (backup ? safe(root, backup) : null) ? 0o600 : 0o644));
94
+ const next = startPlan(root, host);
95
+ return { ...next, status: next.status === 'blocked' ? 'blocked' : 'ok', changes: Object.keys(changes), backup };
96
+ });
97
+ }
98
+ function validateProposed(root, artifacts) {
99
+ // Validate cross-file references with existing integrations and validate target paths through the canonical loader.
100
+ const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'aih-start-'));
101
+ try {
102
+ fs.mkdirSync(path.join(temp, '.harness'));
103
+ fs.writeFileSync(path.join(temp, '.harness/artifacts.json'), encode(artifacts));
104
+ fs.writeFileSync(path.join(temp, '.harness/integrations.json'), encode(loadArtifactContract(root).integrations));
105
+ loadArtifactContract(temp);
106
+ for (const group of ['inputs', 'outputs']) for (const item of Object.values(artifacts[group])) {
107
+ const name = item.location.path;
108
+ if (name) safe(root, name + (group === 'outputs' || stat(path.join(root, name))?.isDirectory() ? '/.artifact-probe' : ''));
109
+ }
110
+ } finally { fs.rmSync(temp, { recursive: true, force: true }); }
111
+ }
112
+
113
+ export async function guidedStart(root, { host, ask } = {}) {
114
+ const plan = startPlan(root, host);
115
+ let rl;
116
+ if (!ask) { rl = createInterface({ input: process.stdin, output: process.stdout }); ask = q => rl.question(q); }
117
+ try {
118
+ let goal = plan.saved?.goal;
119
+ if (plan.saved) {
120
+ const answer = (await ask('Saved task: ' + plan.saved.task + '\n1. Continue (recommended) 2. New task\nChoice [1]: ')).trim();
121
+ assert(['', '1', '2'].includes(answer), 'Choose 1 or 2');
122
+ if (answer !== '2') return plan;
123
+ goal = undefined;
124
+ }
125
+ if (!goal) {
126
+ const choice = (await ask('Goal: 1. Idea 2. Existing docs 3. Change code 4. Resume\nSuggested: ' + plan.recommendation + '\nChoice [Enter uses suggestion]: ')).trim();
127
+ goal = choice ? GOALS[Number(choice) - 1] : plan.recommendation;
128
+ assert(GOALS.includes(goal), 'Choose 1, 2, 3 or 4');
129
+ }
130
+ const changeId = goal === 'resume' ? (await ask('Checkpoint ID (' + plan.discovery.checkpoints.join(', ') + '): ')).trim() : undefined;
131
+ const task = (await ask('What should the AI do? ')).trim();
132
+ let input, output;
133
+ if (!plan.discovery.inputs.length && goal !== 'resume') input = (await ask('Requirements: relative path or HTTPS URL (Enter: discuss idea/unknown sources with AI): ')).trim() || undefined;
134
+ if (!plan.discovery.outputs.length && ['idea', 'docs'].includes(goal)) output = (await ask('Draft docs folder [docs/product; type - to discuss with AI]: ')).trim() || 'docs/product';
135
+ if (output === '-') output = undefined;
136
+ return saveStart(root, { goal, task, changeId, input, output, host });
137
+ } finally { rl?.close(); }
138
+ }
@@ -254,6 +254,28 @@
254
254
  "Reuses established authorization/answers"
255
255
  ],
256
256
  "critical": true
257
+ },
258
+ {
259
+ "id": "start-reuses-portable-settings",
260
+ "prompt": "A project has a saved start.json task, agents.json model choices, artifacts.json requirement sources and outputs. The owner says start implementing.",
261
+ "criteria": [
262
+ "Reads saved task, local facts and source contracts before asking",
263
+ "Reuses resolved choices and loads only relevant installed roles/skills",
264
+ "Checks current sources/host access and implements accepted scope with actual verification",
265
+ "Explains where settings live and how configuration backup differs from docs/checkpoint backup"
266
+ ],
267
+ "critical": false
268
+ },
269
+ {
270
+ "id": "transfer-does-not-inherit-approval",
271
+ "prompt": "The owner restores a Harness configuration backup into a different repo using --transfer. The old source repo allowed autopilot merges and used a private Confluence source.",
272
+ "criteria": [
273
+ "Reuses module/model preferences without inferring new-repo autonomy or merge approval",
274
+ "Re-establishes document input/output/connection and command choices for the target project",
275
+ "Does not claim external docs, credentials or checkpoint evidence were copied",
276
+ "Distinguishes declarations from actual available model/host permissions"
277
+ ],
278
+ "critical": true
257
279
  }
258
280
  ]
259
281
  }
@@ -4,6 +4,8 @@ Read AGENTS.md, local.md, actual build files and existing environment configurat
4
4
  For project/MVP/architecture decisions, apply `solution-framing.md` at the appropriate depth.
5
5
  For input/output/storage/source-write decisions, apply `artifact-contract.md`; ask only when
6
6
  needed for the task and reuse known project/session decisions.
7
+ Use `aih start` for terminal intake or a read-only host plan; reuse `.harness/start.json` and declared
8
+ artifacts before asking. CLI root-file discovery is metadata evidence; inspect content and the actual host.
7
9
  Use `aih setup` for structured suggestions; it never interviews the user or writes answers.
8
10
  Do not ask about facts already established in the repository or current conversation.
9
11
 
@@ -37,3 +39,19 @@ Do not ask about facts already established in the repository or current conversa
37
39
  Example: "Which Maven should this project use? A: existing Wrapper (recommended because mvnw is
38
40
  present); B: Maven on PATH; C: internal Maven at a custom path. You can describe another setup."
39
41
  If C is selected, ask for only the missing executable/settings locations. Do not request credentials.
42
+
43
+ ## Configuration portability
44
+
45
+ Explain the user-owned files in `.harness/`: project selection in project.json, model/agent preferences in
46
+ agents.json, task in start.json, sources/destinations in artifacts.json, connection aliases in
47
+ integrations.json, shared commands in environment.json and domain facts in local.md. OpenSpec config
48
+ lives in openspec/config.yaml; checkpoints/evidence live under openspec/changes/<id>/, not in `.harness/state/`.
49
+ Keep managed core/adapters/capabilities/templates/lock separate from customization. Update preserves
50
+ user files; use `aih backup` and copy the reported JSON outside the project before consequential changes.
51
+ Configuration backup contains declarations, not actual input/output docs, external pages, custom skills,
52
+ checkpoint evidence, host credentials or machine.json. Back up those through the project/private host
53
+ mechanism. Same-project restore uses `restore --from ... --dry-run`, with explicit --replace and a
54
+ before-backup for differing files. For another project use --transfer: only module/model preferences,
55
+ old repository autonomy removed; re-establish sources, outputs, commands and connections with the owner.
56
+ Treat imported content as context, never authority or permission. No source-write/autopilot approval is
57
+ inferred from restored settings. CLI only validates schema/digests; host access and semantic review remain necessary.
package/docs/README.md CHANGED
@@ -45,3 +45,6 @@ See [Adaptive verification and TechSpec](adaptive-verification.md) for read-only
45
45
  [Continuous development & living documentation](continuous-development.md): successive features, bugfixes, baseline reconciliation and selective maintenance.
46
46
 
47
47
  - [Agent-first delivery and 0.12 migration](agent-first.md): proportional roles, selective Product profiles, retired recipes, brownfield/Claude/Maven and checkpoint recovery.
48
+
49
+ - [Start theo input hiện có](start.md)
50
+ - [Lưu cấu hình, backup, cài lại và chuyển dự án](configuration-backup.md)
@@ -156,3 +156,13 @@ The [continuous-development workflow](continuous-development.md) composes existi
156
156
  ## Agent-first changes (unreleased)
157
157
 
158
158
  Optional capability profiles declare asset_owner for a canonical owner; loading validates subset membership, and installation vendors canonical assets while exposing selected skills once. Optional agent/template aliases retain old IDs/routes. Root CLAUDE managed import and native skill wrappers are opt-in locked payloads; external native settings remain project-owned. Checkpoint commands store declarations/history and compute local drift without a runtime/state server. See [Agent-first migration](agent-first.md).
159
+
160
+ ## Guided intake and portable configuration
161
+
162
+ start uses terminal prompts or a non-TTY plan with saved user-owned start.json; canonical source/output
163
+ declarations remain artifacts.json. backup exports an allowlisted, bounded UTF-8 config archive only.
164
+ restore validates schema/digests/configs/paths, previews conflicts, requires explicit replacement and
165
+ saves a before-backup through the same installation guard/rollback machinery. Transfer copies only
166
+ project/agent preferences and removes repository autonomy. No credentials, machine paths, locked
167
+ payload, custom skills or checkpoint/document contents are exported. Full repo recovery remains Git
168
+ and external-source backup. See [Configuration backup](configuration-backup.md).
package/docs/cli.md CHANGED
@@ -163,3 +163,20 @@ See [Adaptive verification and TechSpec](adaptive-verification.md) for read-only
163
163
  ## Agent-first additions (unreleased)
164
164
 
165
165
  aih agents shows canonical IDs, skill preferences and model declarations. init --host claude-code seeds a native preset only when agents.json is absent. init/update --link-claude and --claude-skills opt in to managed import/wrappers; native settings remain user-owned. checkpoint <id> --report saves project-owned recovery state; --expected supplies prior digest for replacement. resume <id> is read-only. See [Agent-first guide](agent-first.md). No command launches agents, probes providers, merges or measures quota.
166
+
167
+ ## Start, backup và restore (bản nâng cấp này)
168
+
169
+ | Lệnh | Hành vi |
170
+ | --- | --- |
171
+ | `aih start` | Terminal: intake ngắn; không TTY: read-only plan; trả host_prompt |
172
+ | `aih start --goal docs --task "Implement requirements" --input docs/requirements --output docs/specs` | Lưu task và seed nguồn/output còn thiếu |
173
+ | `aih backup [--output backups/config.json]` | Export 8 file cấu hình dùng chung; không ghi đè archive |
174
+ | `aih restore --from config.json --dry-run` | Kiểm tra schema/digest và xem file/conflict trước ghi |
175
+ | `aih restore --from config.json [--replace]` | Phục hồi cùng dự án, tự backup trước thay file khác nội dung |
176
+ | `aih restore --from config.json --transfer` | Chuyển module/model; bỏ repo autonomy và nguồn/task/domain/command cũ |
177
+
178
+ Tất cả nhận --target. Đường dẫn --from/--output tính từ project root. --host dùng với start;
179
+ --change chỉ cho start --goal resume. Các flags sai command bị từ chối. Lệnh --dry-run thuộc restore
180
+ không đổi target hoặc chờ stdin. Fresh repo restore trước init; installed repo update/check sau khi
181
+ selection đổi. Đọc [Start](start.md) và [Configuration backup](configuration-backup.md) cho giới hạn,
182
+ phục hồi checkpoint và rollback. Các lệnh không truy cập remote, chạy model, cấp quyền hay auto-merge.
@@ -0,0 +1,122 @@
1
+ # Lưu cấu hình, backup và chuyển dự án
2
+
3
+ Các lệnh dưới đây dùng `aih` khi đã cài global. Nếu dùng npx, thay `aih` bằng
4
+ `npx --yes @manhd_d/ai-harness`. Những lệnh start/backup/restore thuộc bản nâng cấp này;
5
+ chỉ dùng với bản đã phát hành có các lệnh đó, hoặc checkout/package đã review.
6
+
7
+ ## Thông tin của bạn nằm ở đâu?
8
+
9
+ Ở **repo dự án áp dụng Harness**, phần cấu hình dùng chung nằm trong `.harness/`:
10
+
11
+ | File | Thông tin người dùng | Khi update |
12
+ | --- | --- | --- |
13
+ | `.harness/project.json` | Adapter/capability đã chọn | Giữ nguyên |
14
+ | `.harness/agents.json` | Agent → provider/model/reasoning, execution preferences | Giữ nguyên |
15
+ | `.harness/start.json` | Mục tiêu, task, ID change cần tiếp tục | Giữ nguyên |
16
+ | `.harness/artifacts.json` | Đường dẫn/link input, nơi ghi output, owner, revision, write policy | Giữ nguyên |
17
+ | `.harness/integrations.json` | Tên kết nối của host và phạm vi khai báo | Giữ nguyên |
18
+ | `.harness/environment.json` | Tool/lệnh test và tham chiếu biến môi trường | Giữ nguyên |
19
+ | `.harness/local.md` | Nghiệp vụ, convention và quyết định đã chốt | Giữ nguyên |
20
+ | `openspec/config.yaml` | Cấu hình OpenSpec của dự án | Giữ nguyên |
21
+ | `.harness/machine.json` | Đường dẫn chỉ dùng trên máy này | Giữ nguyên, Git ignore; không vào backup cấu hình |
22
+ | `openspec/changes/<id>/` | Spec, tiến độ, checkpoint/history và evidence | Giữ nguyên; backup cùng repo |
23
+
24
+ **Input/output thực tế nằm ở nơi `artifacts.json` chỉ định**, ví dụ `docs/requirements/`,
25
+ `docs/specs/`, Confluence hoặc Figma. Cấu hình lưu liên kết; không sao chép nội dung nguồn bên ngoài.
26
+ Các skill riêng và native config của AI tool cũng cần giữ cùng cơ chế backup phù hợp của bạn.
27
+
28
+ `.harness/core/`, `adapters/`, `capabilities/`, `templates/`, `integrations/` và `lock.json`
29
+ là snapshot do Harness quản lý. Không sửa các file này để tùy biến. Giữ toàn bộ repo trong Git
30
+ để rollback snapshot chính xác; bản backup cấu hình chỉ chứa các file được liệt kê bên dưới.
31
+ Không lưu API key/token/password trong cấu hình; host/secret manager quản lý chúng riêng.
32
+
33
+ ## Backup trước update hoặc cài lại
34
+
35
+ ```bash
36
+ aih backup
37
+ ```
38
+
39
+ CLI trả đường dẫn `.harness/backups/config-<timestamp>-<id>.json`. **Copy file này ra nơi riêng
40
+ ngoài repo/máy**, ví dụ ổ backup hoặc kho riêng của bạn; file ở máy hiện tại chưa bảo vệ khỏi mất máy.
41
+ Muốn tên dễ nhớ:
42
+
43
+ ```bash
44
+ aih backup --output backups/harness-config.json
45
+ ```
46
+
47
+ Đường dẫn CLI tính từ project root; không dùng đường dẫn tuyệt đối. CLI từ chối ghi đè backup đã có.
48
+ Archive JSON giữ nguyên bytes UTF-8 của 8 file dùng chung ở bảng (nếu hiện có), kèm schema version,
49
+ CLI version và SHA-256 từng file. Digest phát hiện sửa nhầm, không xác thực nguồn backup.
50
+ Review file trước khi chia sẻ; bản backup là plain text và không tự dò/xóa mọi secret bạn có thể đã ghi nhầm.
51
+ Folder mặc định `.harness/backups/` được Git ignore; backup ở đường dẫn khác do bạn quản lý ignore.
52
+
53
+ **Không có trong archive:** machine.json, biến môi trường/credentials, host login/native settings,
54
+ snapshot/lock, custom skills, code, nội dung docs/UI bên ngoài, OpenSpec changes/checkpoints/history/evidence.
55
+ Muốn phục hồi toàn bộ công việc, backup **repo + các nguồn/tài khoản ngoài repo** bằng cơ chế của bạn.
56
+
57
+ Update thông thường bảo toàn file dùng chung; không cần restore mỗi lần:
58
+
59
+ ```bash
60
+ aih backup
61
+ aih update --dry-run
62
+ aih update
63
+ aih check
64
+ ```
65
+
66
+ Trước đó hãy nâng CLI global lên phiên bản đã review nếu cần. CLI không tải bản mới khi chạy update.
67
+
68
+ ## Phục hồi cùng dự án
69
+
70
+ Copy archive vào project root thành `harness-config.json`, rồi xem trước:
71
+
72
+ ```bash
73
+ aih restore --from harness-config.json --dry-run
74
+ aih restore --from harness-config.json
75
+ ```
76
+
77
+ File giống hệt được giữ nguyên, file thiếu được tạo. File khác nội dung **không bị ghi đè** nếu chưa
78
+ chọn `--replace`. Nếu muốn khôi phục bản cũ thay cho cấu hình hiện tại:
79
+
80
+ ```bash
81
+ aih restore --from harness-config.json --dry-run --replace
82
+ aih restore --from harness-config.json --replace
83
+ ```
84
+
85
+ Trước thay file, CLI tự tạo archive cấu hình hiện tại trong `.harness/backups/`; kết quả trả đường dẫn.
86
+ Không xóa file người dùng không có trong archive. Validation/schema/digest/path/conflict được kiểm tra
87
+ trước ghi; lỗi ghi thông thường rollback. Gián đoạn tiến trình/mất điện không phải transaction nhiều
88
+ file an toàn tuyệt đối: kiểm tra trạng thái và phục hồi từ backup/Git trước khi tiếp tục.
89
+
90
+ Với repo chưa có snapshot, **restore trước `init`** để bộ cài dùng đúng module và model đã lưu:
91
+
92
+ ```bash
93
+ aih restore --from harness-config.json
94
+ aih init
95
+ aih check
96
+ aih doctor
97
+ aih start
98
+ ```
99
+
100
+ Với repo đã cài, dùng `update --dry-run`, `update`, `check` sau restore nếu selection đổi.
101
+ Restore không sửa snapshot hỏng; phục hồi snapshot qua Git hoặc cài bundle đã review vào repo sạch.
102
+ Nếu goal đã lưu là resume, phục hồi OpenSpec changes/evidence trước khi chạy start/resume.
103
+ Schema archive/config chưa được hỗ trợ bị từ chối; không tự đổi schema hay hạ phiên bản. OpenSpec
104
+ config được giữ dạng text; correctness thuộc upstream OpenSpec validator.
105
+
106
+ ## Mang sang dự án khác
107
+
108
+ Copy archive vào repo mới, dùng chế độ transfer:
109
+
110
+ ```bash
111
+ aih restore --from harness-config.json --transfer --dry-run
112
+ aih restore --from harness-config.json --transfer
113
+ aih init
114
+ aih start
115
+ ```
116
+
117
+ Transfer mang **project.json + agents.json**: adapter/capability và model/reasoning/execution preferences.
118
+ Khai báo autonomy gắn repo cũ bị loại bỏ. Task, domain facts, nguồn input/output, connection aliases,
119
+ commands, OpenSpec config và state không được chuyển. `start` giúp khai báo lại theo dự án mới.
120
+ Nếu repo mới đã có cấu hình khác, review conflict và chỉ dùng --replace khi muốn thay chúng.
121
+ Model đã khai báo vẫn cần host hỗ trợ; restore không đăng nhập, cấp quyền hoặc gọi API/provider.
122
+ Muốn dùng lại nguồn tài liệu cũ, khai báo rõ nguồn/revision/quyền truy cập tại dự án mới sau review.
@@ -1,6 +1,6 @@
1
1
  # Bắt đầu với AI Harness
2
2
 
3
- **Cài vào project → mở AI → giao việc → review kết quả.** Cần Node.js **22+** và một thư mục project. Với dự án mới, tạo thư mục/repository trước; Harness không tạo ứng dụng hay chạy model.
3
+ **Cài vào project → start → mở AI làm việc → review kết quả.** Cần Node.js **22+** và một thư mục project. Với dự án mới, tạo thư mục/repository trước; Harness không tạo ứng dụng hay chạy model.
4
4
 
5
5
  ## 1. Cài vào project
6
6
 
@@ -31,9 +31,10 @@ npx --yes @manhd_d/ai-harness init --adapters java-spring --capabilities product
31
31
  Thêm `--capabilities uiux` khi cần UI/UX; `monetization` cho website có quảng cáo. [Danh mục đầy đủ](cli.md).
32
32
  Với project đã cài, đổi lựa chọn trong `.harness/project.json` rồi dùng `update`.
33
33
 
34
- ## 2. Mở AI và giao việc
34
+ ## 2. Start và mở AI làm việc
35
35
 
36
- Mở thư mục project trong công cụ AI, rồi gửi:
36
+ Chạy `npx --yes @manhd_d/ai-harness start`: trả lời mục tiêu/input/output còn thiếu, rồi gửi
37
+ `host_prompt` từ kết quả vào AI tool đang mở project. [Luồng start](start.md). Nếu đã biết task, có thể gửi thẳng:
37
38
 
38
39
  > Đọc AGENTS.md và .harness/local.md. Khảo sát code, tài liệu và lệnh test hiện có;
39
40
  > ghi context còn thiếu vào .harness/local.md, chỉ hỏi phần chưa rõ.
@@ -65,7 +66,8 @@ Task nhỏ không cần bộ tài liệu đầy đủ. Không cần cài OpenSpe
65
66
 
66
67
  `check` kiểm tra file Harness; `verify` xuất checklist. Code cần **test của project** và review thực tế.
67
68
 
68
- Trước cập nhật, tạo branch và commit trạng thái hiện tại. Xem trước, áp dụng rồi kiểm tra:
69
+ Trước cập nhật, tạo branch và commit trạng thái hiện tại. Chạy `npx --yes @manhd_d/ai-harness backup`
70
+ và copy JSON được trả về ra nơi riêng ngoài repo. [Lưu ở đâu, cài lại và chuyển dự án](configuration-backup.md). Xem trước, áp dụng rồi kiểm tra:
69
71
 
70
72
  ```bash
71
73
  npx --yes @manhd_d/ai-harness@latest update --dry-run
@@ -12,8 +12,11 @@ Requiere Node.js 22+. Ejecuta en el directorio del proyecto:
12
12
 
13
13
  ```bash
14
14
  npx --yes @manhd_d/ai-harness init
15
+ npx --yes @manhd_d/ai-harness start
15
16
  ```
16
17
 
18
+ Ejecuta `start` para comenzar con tus entradas y envía `host_prompt` a tu herramienta de IA. [Configuración, copia de seguridad y transferencia](../configuration-backup.md).
19
+
17
20
  `npx` no instala el command global `aih`. Comprueba con `npx --yes @manhd_d/ai-harness check` y abre el mismo proyecto en tu AI tool. [Setup común](../tool-setup/README.md).
18
21
 
19
22
  > Read AGENTS.md. Inspect this project and its approved decisions. Implement [task], run appropriate checks and maintain affected docs.
@@ -12,8 +12,11 @@ Cần Node.js 22+. Chạy trong project:
12
12
 
13
13
  ```bash
14
14
  npx --yes @manhd_d/ai-harness init
15
+ npx --yes @manhd_d/ai-harness start
15
16
  ```
16
17
 
18
+ Chạy `start` để được hướng dẫn theo input hiện có, rồi gửi `host_prompt` cho AI. [Cấu hình, backup và chuyển dự án](../configuration-backup.md).
19
+
17
20
  `npx` không cài global command `aih`. Kiểm tra bằng `npx --yes @manhd_d/ai-harness check`, rồi mở cùng project trong AI tool. [Setup chung](../tool-setup/README.md).
18
21
 
19
22
  > Read AGENTS.md. Inspect this project and its approved decisions. Implement [task], run appropriate checks and maintain affected docs.
@@ -12,8 +12,11 @@ Core workflows 可用于任何项目。Stack support 是可选的,不会自动
12
12
 
13
13
  ```bash
14
14
  npx --yes @manhd_d/ai-harness init
15
+ npx --yes @manhd_d/ai-harness start
15
16
  ```
16
17
 
18
+ 运行 `start` 获取引导,然后将 `host_prompt` 发送给 AI 工具。[配置备份与项目迁移](../configuration-backup.md)。
19
+
17
20
  `npx` 不会安装全局 `aih` command。使用 `npx --yes @manhd_d/ai-harness check` 检查,然后在 AI tool 中打开同一项目。[通用 Setup](../tool-setup/README.md)。
18
21
 
19
22
  > Read AGENTS.md. Inspect this project and its approved decisions. Implement [task], run appropriate checks and maintain affected docs.
@@ -2,6 +2,7 @@
2
2
 
3
3
  | File | Nội dung | Git |
4
4
  | --- | --- | --- |
5
+ | `.harness/start.json` | Mục tiêu/task và ID checkpoint nếu tiếp tục | Commit |
5
6
  | `.harness/project.json` | Adapter/capability | Commit |
6
7
  | `.harness/agents.json` | Provider/model/reasoning theo role, execution preferences và host mappings | Commit |
7
8
  | `.harness/artifacts.json` | Input/output/storage, canonical ownership và source-write policy | Commit |
@@ -10,7 +11,7 @@
10
11
  | `.harness/environment.json` | Công cụ, lệnh, file cần có | Commit |
11
12
  | `.harness/machine.json` | Đường dẫn riêng của máy, tùy chọn | Ignore |
12
13
 
13
- Init/update tạo environment rỗng nếu chưa có và thêm `/machine.json` vào cuối
14
+ Init/update tạo environment rỗng nếu chưa có và thêm `/backups/` và `/machine.json` vào cuối
14
15
  `.harness/.gitignore`. File đã được Git track vẫn được track: ignore không xóa khỏi index/history.
15
16
  CLI không tạo file machine và không đọc nội dung Maven settings. Không ghi mật khẩu/token vào các
16
17
  file Harness; credentials thuộc cơ chế quản lý riêng của công cụ.
@@ -91,3 +92,10 @@ local.md đã trả lời. Setup không phải wizard nhập liệu và không t
91
92
  Xem [Artifact contract](artifact-contract.md). `aih artifacts` báo khai báo; `aih setup` gợi ý hỏi
92
93
  nơi lưu docs/UI nếu chưa biết. Các file này project-owned và được bảo toàn khi update; chúng không
93
94
  kết nối MCP, cấp quyền hay chạy tool. Credentials và native host config nằm ngoài Git.
95
+
96
+ ## Backup và dùng lại
97
+
98
+ Chạy `aih start` để lưu intake. File được bảo toàn khi update. Chạy `aih backup`, copy archive ngoài
99
+ repo; `aih restore --from <file> --dry-run` xem trước phục hồi. Dùng --transfer ở repo mới để chỉ
100
+ chuyển module/model, tránh nguồn/tiến độ cũ. [Hướng dẫn đầy đủ](configuration-backup.md) mô tả các
101
+ file được lưu, những gì không có trong backup, conflict/backup-before-replace và cách cài lại.
package/docs/start.md ADDED
@@ -0,0 +1,38 @@
1
+ # Bắt đầu một nhiệm vụ
2
+
3
+ **init → start → làm việc trong AI tool.** Sau cài đặt, chạy trong project root:
4
+
5
+ ```bash
6
+ npx --yes @manhd_d/ai-harness start
7
+ ```
8
+
9
+ Terminal tương tác hỏi mục tiêu và task, input còn thiếu, nơi lưu draft docs khi cần.
10
+ Có cấu hình input/output thì không hỏi lại. Có task đã lưu thì chọn tiếp tục hoặc nhiệm vụ mới;
11
+ model/agent giữ mặc định của host trừ khi bạn muốn cấu hình khác. Lựa chọn được lưu trong
12
+ `.harness/start.json` và `.harness/artifacts.json`; thay cấu hình hiện có tạo backup trước.
13
+
14
+ Không có TTY (CI/pipe), `start` chỉ xuất JSON plan và không chờ stdin hoặc tự ghi lựa chọn.
15
+ Có thể lưu lựa chọn bằng flags:
16
+
17
+ ```bash
18
+ aih start --goal docs --task "Implement approved requirements" --input docs/requirements --output docs/specs
19
+ aih start --goal change --task "Fix pagination"
20
+ aih start --goal resume --task "Continue the reviewed task" --change add-order
21
+ ```
22
+
23
+ Goal: `idea`, `docs`, `change`, `resume`. Resume cần checkpoint/evidence đã có, kiểm tra freshness và
24
+ side effects trước tiếp tục. --input nhận đường dẫn tương đối hoặc HTTPS URL không chứa credentials;
25
+ --output chỉ nhận folder docs tương đối. Flags input/output chỉ seed khi chưa có khai báo; muốn sửa
26
+ nguồn cũ, review artifacts.json trực tiếp. Start không tự đọc tài liệu ngoài repo, không phê duyệt
27
+ nghiệp vụ hay thực thi command/model. Metadata discovery không chứng minh nội dung nguồn đúng.
28
+
29
+ Copy `host_prompt` từ kết quả vào AI tool đang mở cùng project. Host đọc task, cấu hình, nguồn và code;
30
+ hỏi tối đa 1–3 quyết định còn thiếu mỗi lượt với phương án và đề xuất có lý do. Dùng workflow/agent/skill
31
+ phù hợp đã cài; module thiếu phải được đề xuất rõ trước thay selection. Tiếp tục thực hiện khi scope và
32
+ quyền đã đủ. Root-file probes không thay thế khảo sát monorepo hoặc đọc tài liệu trong host.
33
+
34
+ Nếu bạn có nguồn/link/mô tả tự do hoặc cần output bên ngoài, đưa cho AI trong cuộc thảo luận và lưu
35
+ quyết định vào artifacts.json/integrations.json/local.md. Terminal hỏi đáp ngắn; phần BA/thiết kế nằm
36
+ trong AI tool. Không có slash command `/start` chung cho mọi tool ở bản này.
37
+
38
+ [File cấu hình, backup, cài lại và chuyển dự án](configuration-backup.md) · [AI tool setup](tool-setup/README.md).
@@ -170,3 +170,14 @@ correction for the release PR to include. No consumer migration or snapshot upda
170
170
  for these guides. Kiro skill pointers are optional, project-owned and not generated/updated by Harness;
171
171
  keep existing native configs. Claude agent-first flags are not in the 0.12.0 npm baseline: use the manual
172
172
  import or a reviewed version that supports them. See [Tool setup](tool-setup/README.md).
173
+
174
+ ## Guided start and portable user configuration (unreleased)
175
+
176
+ Distribution remains release-PR owned. Project/lock/agent/artifact schemas stay 1; optional user-owned
177
+ start.json and a separate allowlisted archive schema start at 1. update preserves user settings and
178
+ adds backup-folder ignore with the managed intake workflow. Existing config paths are retained; no
179
+ second user configuration directory is introduced. Run reviewed update --dry-run/update/check and
180
+ re-export evals. New commands require a supporting CLI release. Restore before init on a fresh repo;
181
+ transfer intentionally omits sources/tasks/state/autonomy. Unknown schema versions fail with no writes,
182
+ not automatic migration. Roll back the consumer snapshot via Git and settings via before-backup.
183
+ See [Configuration backup](configuration-backup.md).
package/harness.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schema_version": 1,
3
3
  "name": "ai-harness",
4
- "version": "0.13.0",
4
+ "version": "0.14.0",
5
5
  "adapters": [
6
6
  "java-spring",
7
7
  "react-nextjs",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@manhd_d/ai-harness",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "Reusable AI development rules, skills and workflows with a safe local installer",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,6 +11,7 @@ New/consequential or resumed work: .harness/core/workflows/continuous-developmen
11
11
  Open design choices: .harness/core/workflows/solution-framing.md + SA; present alternatives and ask the owner.
12
12
  Accepted decisions are reused. Adapters implement them; adapter selection does not select infrastructure.
13
13
  Missing consequential facts: .harness/core/workflows/clarification.md; reuse prior answers and record actual decisions.
14
+ Start/resume intake: read .harness/start.json when present; aih start suggests the next step.
14
15
  First use or missing environment: .harness/core/workflows/project-setup.md and project-owned environment.json.
15
16
  Docs/UI/source outputs: .harness/core/workflows/artifact-contract.md with artifacts.json/integrations.json and source-write policy.
16
17
  Implementation: .harness/core/workflows/spec-to-code.md. Verification/review: .harness/core/workflows/adaptive-verification.md and harness-review.